342 lines
10 KiB
JavaScript
342 lines
10 KiB
JavaScript
/* exported app, utils */
|
|
|
|
const kanas = {
|
|
columns: [ 'A', 'I', 'U', 'E', 'O' ],
|
|
rows: [
|
|
'',
|
|
'K',
|
|
'S',
|
|
'T',
|
|
'N',
|
|
'H',
|
|
'M',
|
|
'Y',
|
|
'R',
|
|
'W',
|
|
'(N)',
|
|
],
|
|
hiraganas: [
|
|
'あいうえお',
|
|
'かきくけこ',
|
|
'さしすせそ',
|
|
'たちつてと',
|
|
'なにぬねの',
|
|
'はひふへほ',
|
|
'まみむめも',
|
|
'や ゆ よ',
|
|
'らりるれろ',
|
|
'わ を',
|
|
'ん',
|
|
],
|
|
katakanas: [
|
|
'アイウエオ',
|
|
'カキクケコ',
|
|
'サシスセソ',
|
|
'タチツテト',
|
|
'ナニヌネノ',
|
|
'ハヒフヘホ',
|
|
'マミムメモ',
|
|
'ヤ ユ ヨ',
|
|
'ラリルレロ',
|
|
'ワ ヲ',
|
|
'ン',
|
|
],
|
|
exceptions: {
|
|
SI: 'SHI',
|
|
TI: 'CHI',
|
|
TU: 'TSU',
|
|
HU: 'FU',
|
|
},
|
|
traps: {
|
|
hiraganas: [
|
|
'うえふら',
|
|
'いりにけはほ',
|
|
'あおぬめ',
|
|
'れねわ',
|
|
'しも',
|
|
'つてと',
|
|
'るろそ',
|
|
'まはほよ',
|
|
'くへ',
|
|
'たなを',
|
|
'えん',
|
|
'きさち',
|
|
],
|
|
katakanas: [
|
|
'ンソ',
|
|
'シシ',
|
|
'ノメ',
|
|
'クケタ',
|
|
'コロヨ',
|
|
'ワウフ',
|
|
'ヌフ',
|
|
'ネホオ',
|
|
'チテ',
|
|
'エニ',
|
|
'コユ',
|
|
'ラテ',
|
|
'セヒ',
|
|
],
|
|
},
|
|
mappings: [
|
|
[ 0, 1 ],
|
|
[ 1, 0 ],
|
|
[ 0, 2 ],
|
|
[ 2, 0 ],
|
|
[ 1, 2 ],
|
|
[ 2, 1 ],
|
|
],
|
|
};
|
|
|
|
const utils = {
|
|
randint: function (min, max) {
|
|
return Math.floor(Math.random() * (max - min)) + min;
|
|
},
|
|
randindex: function (array, ...toIgnore) {
|
|
if (array.length === 0 || toIgnore.length >= array.length) {
|
|
return -1;
|
|
}
|
|
let index;
|
|
do {
|
|
index = this.randint(0, array.length);
|
|
} while (this.contains(toIgnore, index));
|
|
return index;
|
|
},
|
|
randitem: function (array, ...toIgnore) {
|
|
if (array.length === 0) {
|
|
return undefined;
|
|
}
|
|
return array[this.randindex(array, ...toIgnore)];
|
|
},
|
|
randindexes: function (array, number, ...toIgnore) {
|
|
number = Math.min(number, array.length - toIgnore.length);
|
|
const output = [];
|
|
for (let i = 0; i < number; i++) {
|
|
output.push(this.randindex(array, ...output, ...toIgnore));
|
|
}
|
|
return output;
|
|
},
|
|
randitems: function (array, number) {
|
|
const indexes = this.randindexes(array, number);
|
|
return indexes.map(i => array[i]);
|
|
},
|
|
shuffle: function (array) {
|
|
const output = [ ...array ];
|
|
for (let i = 0; i < array.length; i++) {
|
|
const i1 = this.randindex(array);
|
|
const i2 = this.randindex(array, i1);
|
|
[ output[i1], output[i2] ] = [ output[i2], output[i1] ];
|
|
}
|
|
return output;
|
|
},
|
|
contains: function (array, item) {
|
|
return array.indexOf(item) >= 0;
|
|
},
|
|
unique: function (array) {
|
|
return [ ...new Set(array) ];
|
|
},
|
|
};
|
|
|
|
Array.prototype.shuffle = function () {
|
|
return utils.shuffle(this);
|
|
};
|
|
Array.prototype.contains = function (item) {
|
|
return utils.contains(this, item);
|
|
};
|
|
Array.prototype.unique = function () {
|
|
return utils.unique(this);
|
|
};
|
|
|
|
const cookies = {
|
|
set: function (name, value, days = undefined) {
|
|
const maxAge = !days ? undefined : days * 864e2;
|
|
document.cookie = `${name}=${encodeURIComponent(value)}${maxAge ? `;max-age=${maxAge};` : ''}`;
|
|
},
|
|
get: function (name) {
|
|
return document.cookie.split('; ').reduce(function (r, v) {
|
|
const parts = v.split('=');
|
|
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
|
|
}, '');
|
|
},
|
|
};
|
|
|
|
let app = {
|
|
el: '#app',
|
|
data: {
|
|
title: 'Kana Test',
|
|
score: 0,
|
|
highscore: 0,
|
|
savedHighscores: {},
|
|
options: {
|
|
available: kanas.rows,
|
|
selected: kanas.rows,
|
|
mappings: [ 0, 1 ],
|
|
answers: 6,
|
|
},
|
|
kanas: [],
|
|
question: 'あ',
|
|
lastQuestions: [ '', '', '' ],
|
|
answers: [ 'A', 'I', 'U', 'O' ],
|
|
wrongAnswers: [],
|
|
},
|
|
watch: {
|
|
options: {
|
|
handler: 'changeOption',
|
|
deep: true,
|
|
},
|
|
},
|
|
methods: {
|
|
findKana: function(v) {
|
|
const self = this;
|
|
return self.kanas.filter((kana) => {
|
|
return kana.contains(v);
|
|
})[0];
|
|
},
|
|
buildKanas: function () {
|
|
const self = this;
|
|
self.kanas = [];
|
|
self.options.selected.forEach((prefix) => {
|
|
const row = kanas.rows.indexOf(prefix);
|
|
if (prefix === '(N)') {
|
|
self.kanas.push([ 'N', kanas.hiraganas[row][0], kanas.katakanas[row][0], row, 0 ]);
|
|
} else {
|
|
kanas.columns.forEach((suffix, column) => {
|
|
const text = kanas.exceptions[prefix + suffix]
|
|
? kanas.exceptions[prefix + suffix]
|
|
: prefix + suffix;
|
|
if (kanas.hiraganas[row][column] !== ' ') {
|
|
self.kanas.push([ text, kanas.hiraganas[row][column], kanas.katakanas[row][column], row, column ]);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
findSimilars: function(kanaIndex, mapping) {
|
|
const self = this;
|
|
const kana = self.kanas[kanaIndex];
|
|
const similarIndexes = [];
|
|
if (mapping.contains(1)) {
|
|
const traps = kanas.traps.hiraganas.filter(line => line.includes(kana[1]));
|
|
utils.randitems(traps, 2).forEach(line => {
|
|
const trapKana = utils.randitem(line.split(''), line.indexOf(kana[1]));
|
|
similarIndexes.push(self.kanas.indexOf(self.findKana(trapKana)));
|
|
});
|
|
}
|
|
if (mapping.contains(2)) {
|
|
const traps = kanas.traps.katakanas.filter(line => line.includes(kana[2]));
|
|
utils.randitems(traps, 2).forEach(line => {
|
|
const trapKana = utils.randitem(line.split(''), line.indexOf(kana[2]));
|
|
similarIndexes.push(self.kanas.indexOf(self.findKana(trapKana)));
|
|
});
|
|
}
|
|
if (kana[0] !== 'N') {
|
|
const sameRow = self.kanas.filter((kana2, i) => !similarIndexes.contains(i) && kana2 !== kana && kana2[3] === kana[3]);
|
|
if (sameRow.length > 0) {
|
|
similarIndexes.push(self.kanas.indexOf(utils.randitem(sameRow)));
|
|
}
|
|
const sameColumn = self.kanas.filter((kana2, i) => !similarIndexes.contains(i) && kana2 !== kana && kana2[4] === kana[4]);
|
|
if (sameColumn.length > 0) {
|
|
similarIndexes.push(self.kanas.indexOf(utils.randitem(sameColumn)));
|
|
}
|
|
}
|
|
return similarIndexes.unique().filter(i => i >= 0);
|
|
},
|
|
generateQuestion: function () {
|
|
const self = this;
|
|
|
|
const mapping = kanas.mappings[utils.randitem(self.options.mappings)];
|
|
|
|
const questionIndex = utils.randindex(self.kanas, self.lastQuestions);
|
|
|
|
self.lastQuestions.pop(0);
|
|
self.lastQuestions.push(questionIndex);
|
|
|
|
const similarIndexes = self.findSimilars(questionIndex, mapping);
|
|
const otherIndexes = utils.randindexes(self.kanas, Math.max(0, self.options.answers - similarIndexes.length - 1), questionIndex, ...similarIndexes );
|
|
|
|
self.question = self.kanas[questionIndex][mapping[0]];
|
|
self.answers = [ questionIndex ]
|
|
.concat(similarIndexes)
|
|
.concat(otherIndexes)
|
|
.map((index) => self.kanas[index][mapping[1]]);
|
|
self.answers = self.answers.shuffle();
|
|
self.wrongAnswers = [];
|
|
|
|
/*
|
|
* setTimeout(() => {
|
|
* this.answer(self.question);
|
|
* });
|
|
*/
|
|
},
|
|
answer: function (v) {
|
|
const self = this;
|
|
|
|
const question = self.findKana(self.question);
|
|
|
|
if (question.contains(v)) {
|
|
self.score += 1;
|
|
if (self.score > self.highscore) {
|
|
self.highscore = self.score;
|
|
self.savedHighscores[btoa(JSON.stringify(self.options))] = self.highscore;
|
|
cookies.set('highscores', btoa(JSON.stringify(self.savedHighscores)));
|
|
}
|
|
self.generateQuestion();
|
|
} else {
|
|
self.score = 0;
|
|
self.wrongAnswers.push(v);
|
|
}
|
|
|
|
document.activeElement.blur();
|
|
},
|
|
changeOption: function () {
|
|
const self = this;
|
|
|
|
if (self.options.mappings.length === 0) {
|
|
self.options.mappings.push(0);
|
|
}
|
|
if (self.options.selected.length === 0) {
|
|
self.options.selected.push('');
|
|
}
|
|
|
|
cookies.set('options', btoa(JSON.stringify(self.options)));
|
|
|
|
self.highscore = self.savedHighscores[btoa(JSON.stringify(self.options))] ?? 0;
|
|
self.score = 0;
|
|
self.buildKanas();
|
|
self.generateQuestion();
|
|
},
|
|
readCookies: function() {
|
|
const self = this;
|
|
|
|
const savedOptions = cookies.get('options');
|
|
if (savedOptions) {
|
|
try {
|
|
self.options = JSON.parse(atob(savedOptions));
|
|
} catch (e) { /* ignored */ }
|
|
}
|
|
|
|
const savedHighscores = cookies.get('highscores');
|
|
if (savedHighscores) {
|
|
try {
|
|
self.savedHighscores = JSON.parse(atob(savedHighscores));
|
|
self.highscore = self.savedHighscores[btoa(JSON.stringify(self.options))] ?? 0;
|
|
} catch (e) { /* ignored */ }
|
|
}
|
|
},
|
|
},
|
|
mounted: function () {
|
|
const self = this;
|
|
console.log('app mounted');
|
|
setTimeout(() => {
|
|
self.$el.setAttribute('style', '');
|
|
});
|
|
self.buildKanas();
|
|
self.readCookies();
|
|
self.generateQuestion();
|
|
},
|
|
};
|
|
|
|
window.onload = () => {
|
|
app = new Vue(app);
|
|
};
|