├── .appveyor.yml ├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── language_cards └── setup ├── cards └── en │ ├── chinese-hsk1-pinyin.yml │ ├── japanese-hiragana.yml │ ├── japanese-kanji-jlpt-n5.yml │ ├── japanese-katakana-keyboard-mappings.yml │ └── japanese-katakana.yml ├── language_cards.gemspec ├── lib ├── language_cards.rb └── language_cards │ ├── card_set_builder.rb │ ├── controllers │ ├── application_controller.rb │ ├── game.rb │ └── main_menu.rb │ ├── defaults.rb │ ├── helpers │ ├── game_helper.rb │ └── view_helper.rb │ ├── menu_builder.rb │ ├── menu_node.rb │ ├── models │ ├── card.rb │ └── card_set.rb │ ├── modes │ ├── game.rb │ ├── translate.rb │ └── typing_practice.rb │ ├── timer.rb │ ├── user_interface.rb │ ├── version.rb │ ├── view │ ├── game.erb │ └── main_menu.erb │ └── yaml_loader.rb ├── locales └── en.yml └── test ├── card_set_builder_test.rb ├── card_set_test.rb ├── grapheme_test.rb ├── language_cards_test.rb ├── mappings_test.rb ├── support.rb ├── test_helper.rb ├── timer_test.rb └── user_interface_test.rb /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # appveyor.yml 2 | install: 3 | - set PATH=C:\Ruby23-x64\bin;%PATH% 4 | - bundle install 5 | 6 | build: off 7 | 8 | before_test: 9 | - ruby -v 10 | - gem -v 11 | - bundle -v 12 | 13 | test_script: 14 | - bundle exec rake 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | **/*.swp 11 | mkmf.log 12 | *.gem 13 | .byebug_history 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4.0 5 | before_install: gem install bundler -v 1.13.7 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in language_cards.gemspec 4 | gemspec 5 | 6 | gem 'simplecov', :require => false, :group => :test 7 | 8 | group :development do 9 | gem 'byebug' 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2018 Daniel P. Clark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LanguageCards 2 | [![Gem Version](https://badge.fury.io/rb/language_cards.svg)](https://badge.fury.io/rb/language_cards) 3 | [![Build Status](https://travis-ci.org/danielpclark/language_cards.svg?branch=master)](https://travis-ci.org/danielpclark/language_cards) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/y6jadvlhk50ncbrh?svg=true)](https://ci.appveyor.com/project/danielpclark/language-cards) 5 | 6 | 7 | This is a flash card game for guessing translations or keyboard learning. Currently implemented is 8 | English Romaji to Japanese Hirigana/Katakana and typing exercises for each. Game experience will be improved upon! 9 | 10 | Also if your interested in adding other flash cards for language learning, pull requests are welcome. Please 11 | keep some sort of organized collection for sets of cards (for instance the Japanese-Language Proficiency Test 12 | has different levels to complete, N1 through N5, which would each count as a collection). 13 | 14 | Internationalization support is built in! Translators are welcome to make this game available in other languages. 15 | 16 | ## Installation 17 | 18 | Install it yourself as: 19 | 20 | $ gem install language_cards 21 | 22 | Or try out the latest master by downloading it: [[master.zip](https://github.com/danielpclark/language_cards/archive/master.zip)] 23 | 24 | ## Usage 25 | 26 | After installing the gem you can run the executable `language_cards`. If you clone the repo then use 27 | `bundle exec bin/language_cards`. 28 | 29 | # Card Format 30 | 31 | The cards are stored in YAML format. You can look in the `cards` directory for existing examples to follow. 32 | The first entry is a language name and it's okay if that already exists in another file. The entries below that 33 | must be unique for that language (eg: you can't have two Hiragana sub entries on Japanese). The next step in 34 | will have a mapping hash on how the language is being mapped in the form of key to value (eg "Hiragana" => "Romaji"). 35 | Just follow the below outline for a working example. 36 | 37 | ```yaml 38 | --- 39 | Japanese: 40 | Hiragana: 41 | mapping: { Hiragana: Romaji } 42 | あ: a 43 | い: i 44 | う: u 45 | え: e 46 | お: o 47 | ``` 48 | 49 | ## Development 50 | 51 | *Tests required moving forward with this project unless it's translation files.* 52 | 53 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 54 | 55 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 56 | 57 | ## Contributing 58 | 59 | Bug reports and pull requests are welcome on GitHub at https://github.com/danielpclark/language_cards. 60 | Translations of the game itself are kept in the `locales` folder. Flash cards are stored in YAML format in the`cards` folder. 61 | 62 | 63 | ## License 64 | 65 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 66 | 67 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "language_cards" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/language_cards: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $: << File.expand_path(File.join('..', '..', 'lib'), __FILE__) 4 | 5 | require 'language_cards' 6 | 7 | LanguageCards.start 8 | 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /cards/en/chinese-hsk1-pinyin.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Chinese-HKS1: 3 | Pinyin: 4 | mapping: { Chinese: Pinyin } 5 | # HSK 1 6 | # https://www.hsk.academy/en/hsk_1 7 | 爱: [ài, ai4] 8 | 八: [bā, ba1] 9 | 爸爸: [bàba, ba4ba] 10 | 杯子: [bēizi, bei1zi] 11 | 北京: [běijīng, bei3jing1] 12 | 本: [běn, ben3] 13 | 不客气: [búkèqi, bu2ke4qi] 14 | 不: [bù, bu4] 15 | 菜: [cài, cai4] 16 | 茶: [chá, cha2] 17 | 吃: [chī, chi1] 18 | 出租车: [chūzūchē, chu1zu1che1] 19 | 打电话: [dǎdiànhuà, da3dian4hua4] 20 | 大: [dà, da4] 21 | 的: de 22 | 点: [diǎn, dian3] 23 | 电脑: [diànnǎo, dian4nao3] 24 | 电视: [diànshì, dian4shi4] 25 | 电影: [diànyǐng, dian4ying3] 26 | 东西: [dōngxi, dong1xi] 27 | 都: [dōu, dou1] 28 | 读: [dú, du2] 29 | 对不起: [duìbuqǐ, bui4buqi3] 30 | 多: [duō, duo1] 31 | 多少: [duōshǎo, duo1shao3] 32 | 儿子: [érzi, er2zi] 33 | 二: [èr, er4] 34 | 饭店: [fàndiàn, fan4dian4] 35 | 飞机: [fēijī, fei1ji1] 36 | 分钟: [fēnzhōng, fen1zhong1] 37 | 高兴: [gāoxìng, gao1xing4] 38 | 个: [gè, ge4] 39 | 工作: [gōngzuò, gong1zuo4] 40 | 狗: [gǒu, gou3] 41 | 汉语: [hànyǔ, han4yu3] 42 | 好: [hǎo, hao3] 43 | 号: [hào, hao4] 44 | 喝: [hē, he1] 45 | 和: [hé, he2] 46 | 很: [hěn, hen3] 47 | 后面: [hòumiàn, hou4mian4] 48 | 回: [huí, hui2] 49 | 会: [huì, hui4] 50 | 几: [jǐ, ji3] 51 | 家: [jiā, jia1] 52 | 叫: [jiào, jiao4] 53 | 今天: [jīntiān, jin1tian1] 54 | 九: [jiǔ, jiu3] 55 | 开: [kāi, kai1] 56 | 看: [kàn, kan4] 57 | 看见: [kànjiàn, kan4jian4] 58 | 块: [kuài, kuai4] 59 | 来: [lái, lai2] 60 | 老师: [lǎoshī, lao3shi1] 61 | 了: le 62 | 冷: [lěng, leng3] 63 | 里: [lǐ, li3] 64 | 六: [liù, liu4] 65 | 吗: ma 66 | 妈妈: [māma, ma1ma] 67 | 买: [mǎi, mai3] 68 | 猫: [māo, mao1] 69 | 没关系: [méiguānxi, mei2guan1xi] 70 | 没有: [méiyǒu, mei2you3] 71 | 米饭: [mǐfàn, mi3fan4] 72 | 名字: [míngzi, ming2zi] 73 | 明天: [míngtiān, ming2tian1] 74 | 哪: [nǎ, na3] 75 | 哪儿: [nǎer, na3er] 76 | 那: [nà, na4] 77 | 呢: ne 78 | 能: [néng, neng2] 79 | 你: [nǐ, ni3] 80 | 年: [nián, nian2] 81 | 女儿: [nǚér, nv3er2] 82 | 朋友: [péngyou, peng2you] 83 | 漂亮: [piàoliang, piao4liang] 84 | 苹果: [píngguǒ, ping2guo3] 85 | 七: [qī, qi1] 86 | 前面: [qiánmiàn, qian2mian4] 87 | 钱: [qián, qian2] 88 | 请: [qǐng, qing3] 89 | 去: [qù, qu4] 90 | 热: [rè, re4] 91 | 人: [rén, ren2] 92 | 认识: [rènshi, ren4shi] 93 | 三: [sān, san3] 94 | 商店: [shāngdiàn, shang1dian4] 95 | 上: [shàng, shang4] 96 | 上午: [shàngwǔ, shang4wu3] 97 | 少: [shǎo, shao3] 98 | 谁: [shéi, shei2] 99 | 什么: [shénme, shen2me] 100 | 十: [shí, shi2] 101 | 时候: [shíhou, shi2hou] 102 | 是: [shì, shi4] 103 | 书: [shū, shu1] 104 | 水: [shuǐ, shui3] 105 | 水果: [shuǐguǒ, shui3guo3] 106 | 睡觉: [shuìjiào, shui4jiao4] 107 | 说: [shuō, shuo1] 108 | 四: [sì, si4] 109 | 岁: [suì, sui4] 110 | 他: [tā, ta1] 111 | 她: [tā, ta1] 112 | 太: [tài, tai4] 113 | 天气: [tiānqì, tian1qi4] 114 | 听: [tīng, ting1] 115 | 同学: [tóngxué, tong2xue2] 116 | 喂: [wèi, wei4] 117 | 我: [wǒ, wo3] 118 | 我们: [wǒmen, wo3men] 119 | 五: [wǔ, wu3] 120 | 喜欢: [xǐhuan, xi3huan] 121 | 下: [xià, xia4] 122 | 下午: [xiàwǔ, xia4wu3] 123 | 下雨: [xiàyǔ, xia4yu3] 124 | 先生: [xiānsheng, xian1sheng] 125 | 现在: [xiànzài, xian4zai4] 126 | 想: [xiǎng, xiang3] 127 | 小: [xiǎo, xiao3] 128 | 小姐: [xiǎojie, xiao3jie] 129 | 些: [xiē, xie1] 130 | 写: [xiě, xie3] 131 | 谢谢: [xièxie, xie4xie] 132 | 星期: [xīngqī, xing1qi1] 133 | 学生: [xuésheng, xue2sheng] 134 | 学习: [xuéxí, xue2xi2] 135 | 学校: [xuéxiào, xue2xiao4] 136 | 一: [yī, yi1] 137 | 一点儿: [yīdiǎner, yi1dian3er] 138 | 医生: [yīshēng, yi1sheng1] 139 | 医院: [yīyuàn, yi1yuan4] 140 | 衣服: [yīfu, yi1fu] 141 | 椅子: [yǐzi, yi3zi] 142 | 有: [yǒu, you3] 143 | 月: [yuè, yue4] 144 | 再见: [zàijiàn, zai4jian4] 145 | 在: [zài, zai4] 146 | 怎么: [zěnme, zen3me] 147 | 怎么样: [zěnmeyàng, zen3meyang4] 148 | 这: [zhè, zhe4] 149 | 中国: [zhōngguó, zhong1guo2] 150 | 中午: [zhōngwǔ, zhong1wu3] 151 | 住: [zhù, zhu4] 152 | 桌子: [zhuōzi, zhuo1zi] 153 | 字: [zì, zi4] 154 | 昨天: [zuótiān, zuo2tian1] 155 | 做: [zuò, zuo4] 156 | 坐: [zuò, zuo4] 157 | 158 | -------------------------------------------------------------------------------- /cards/en/japanese-hiragana.yml: -------------------------------------------------------------------------------- 1 | --- # https://en.wikibooks.org/wiki/Japanese/Kana_chart 2 | # https://www.nayuki.io/page/variations-on-japanese-romanization 3 | Japanese: 4 | Hiragana: 5 | mapping: { Hiragana: Romaji } 6 | あ: a 7 | い: i 8 | う: u 9 | え: e 10 | お: o 11 | か: ka 12 | き: ki 13 | く: ku 14 | け: ke 15 | こ: ko 16 | が: ga 17 | ぎ: gi 18 | ぐ: gu 19 | げ: ge 20 | ご: go 21 | さ: sa 22 | す: su 23 | せ: se 24 | そ: so 25 | ざ: za 26 | ず: zu 27 | ぜ: ze 28 | ぞ: zo 29 | じゃ: [zya, ja, jya] 30 | じゅ: [zyu, ju, jyu] 31 | じょ: [zyo, jo, jyo] 32 | た: ta 33 | て: te 34 | と: to 35 | だ: da 36 | で: de 37 | ど: do 38 | な: na 39 | に: ni 40 | ぬ: nu 41 | ね: ne 42 | の: 'no' 43 | は: ha 44 | ひ: hi 45 | へ: he 46 | ほ: ho 47 | ば: ba 48 | び: bi 49 | ぶ: bu 50 | べ: be 51 | ぼ: bo 52 | ぱ: pa 53 | ぴ: pi 54 | ぷ: pu 55 | ぺ: pe 56 | ぽ: po 57 | ふ: [hu, fu] 58 | ま: ma 59 | み: mi 60 | む: mu 61 | め: me 62 | も: mo 63 | や: ya 64 | ゆ: yu 65 | よ: yo 66 | ら: ra 67 | り: ri 68 | る: ru 69 | れ: re 70 | ろ: ro 71 | わ: wa 72 | を: wo 73 | ん: n 74 | つ: [tu, tsu] 75 | きゃ: kya 76 | きゅ: kyu 77 | きょ: kyo 78 | ぎゃ: gya 79 | ぎゅ: gyu 80 | ぎょ: gyo 81 | しゃ: [sya, sha] 82 | し: [si, shi] 83 | しゅ: [syu, shu] 84 | しょ: [syo, sho] 85 | ちゃ: [tya, cha] 86 | ち: [ti, chi] 87 | ちゅ: [tyu, chu] 88 | ちょ: [tyo, cho] 89 | にゃ: nya 90 | にゅ: nyu 91 | にょ: nyo 92 | ひゃ: hya 93 | ひゅ: hyu 94 | ひょ: hyo 95 | びゃ: bya 96 | びゅ: byu 97 | びょ: byo 98 | ぴゃ: pya 99 | ぴゅ: pyu 100 | ぴょ: pyo 101 | みゃ: mya 102 | みゅ: myu 103 | みょ: myo 104 | りゃ: rya 105 | りゅ: ryu 106 | りょ: ryo 107 | じ: [zi, ji] 108 | ぢ: [di, ji, dji] 109 | づ: [du, zu, dzu] 110 | ゔ: vu # https://en.wiktionary.org/wiki/%E3%82%94 111 | ぢゃ: [dya, ja, dja] 112 | ぢゅ: [dyu, ju, dju] 113 | ぢょ: [dyo, jo, djo] 114 | -------------------------------------------------------------------------------- /cards/en/japanese-kanji-jlpt-n5.yml: -------------------------------------------------------------------------------- 1 | --- # https://www.jlptstudy.net/N5/?kanji-list 2 | Japanese: 3 | Kanji (JLPT Level N5): 4 | mapping: { Kanji: [Hiragana, English] } 5 | 一: [ひと, ひとつ, one, only, even, 1] 6 | 七: [なな, seven, 7] 7 | 万: [よろず, ten thousand] 8 | 三: [さん, three, 3] 9 | 上: [うえ, above, up] 10 | 下: [した, below, down, descend, give, low, inferior] 11 | 中: [なか, in, inside, middle, mean, center] 12 | 九: [ここの, nine, 9] 13 | 二: [に, two, 2] 14 | 五: [ご, five, 5] 15 | 人: [ひと, people, person, man, human being, other people, mankind] 16 | 今: [いま, now] 17 | 休: [きゅう, rest, day off, retire, sleep] 18 | 会: [かい, gathering, meeting, meet, party, association, interview, join] 19 | 何: [なに, なん, what] 20 | 先: [さき, before, ahead, previous, future, precedence] 21 | 入: [いり, enter, insert] 22 | 八: [はち, eight, 8] 23 | 六: [ろく, six, 6] 24 | 円: [えん, circle, yen, round] 25 | 出: [で, exit, leave] 26 | 分: [ぶん, part, minute of time, segment, share, degree, one's lot, duty, understand, know, rate, 1%, chances, shaku/100] 27 | 前: [まえ, in front, before] 28 | 北: [きた, north] 29 | 十: [じゅう, ten, 10] 30 | 千: [せん, thousand, 1000] 31 | 午: [うま, noon, sign of the horse, 11AM-1PM, seventh sign of Chinese zodiac] 32 | 半: [はん, half, middle, odd number, semi-, part-] 33 | 南: [みなみ, south] 34 | 友: [とも, friend] 35 | 口: [くち, mouth] 36 | 古: [ふる, old] 37 | 右: [みぎ, right] 38 | 名: [な, name, noted, distinguished, reputation] 39 | 四: [よん, four, 4] 40 | 国: [くに, country, state, region, province, home, hometown, homeland, home country, land, earth] 41 | 土: [つち, soil, earth, ground, Turkey] 42 | 外: [そと, outside, outside of] 43 | 多: [さわ, many, frequent, much] 44 | 大: [おお, large, big] 45 | 天: [てん, heavens, sky, imperial] 46 | 女: [おんな, woman, female] 47 | 子: [こ, child, sign of the rat, 11PM-1AM, first sign of Chinese zodiac] 48 | 学: [がく, study, learning, science] 49 | 安: [あん, cheap, relax, low, quiet, rested, contented, peaceful] 50 | 小: [しょう, little, small] 51 | 少: [すくな, few, little] 52 | 山: [やま, mountain] 53 | 川: [かわ, stream, river] 54 | 左: [ひだり, left] 55 | 年: [とし, year, counter for years, age] 56 | 店: [みせ, store, shop] 57 | 後: [あと, behind, back, later] 58 | 手: [て, hand] 59 | 新: [しん, new] 60 | 日: [ひ, day, sun, Japan, counter for days, sunshine, sunlight, case, event] 61 | 時: [じ, time, hour] 62 | 書: [しょ, write] 63 | 月: [つき, month, moon] 64 | 木: [き, tree, wood] 65 | 本: [もと, book, present, main, 'true', real, counter for long cylindrical things] 66 | 来: [き, come, due, next, cause, become] 67 | 東: [ひがし, east] 68 | 校: [-こう, exam, school, printing, proof, correction] 69 | 母: [はは, mama, mother] 70 | 毎: [まい, every] 71 | 気: [いき, spirit, mind, air, atmosphere, mood] 72 | 水: [みず, water] 73 | 火: [ひ, fire] 74 | 父: [ちち, father] 75 | 生: [なま, life, genuine, birth] 76 | 男: [おとこ, male] 77 | 白: [しろ, white] 78 | 百: [もも, hundred, 100] 79 | 目: [め, eye, class, look, insight, experience, care, favor] 80 | 社: [やしろ, company, firm, office, association, shrine] 81 | 空: [そら, empty, sky, void, vacant, vacuum] 82 | 立: [たて, stand up] 83 | 耳: [みみ, ear] 84 | 聞: [きき, hear, ask, listen] 85 | 花: [はな, flower] 86 | 行: [こう, going, journey] 87 | 西: [にし, west, Spain] 88 | 見: [み, see, hopes, chances, idea, opinion, look at, visible] 89 | 言: [げん, say] 90 | 話: [はなし, tale, talk] 91 | 語: [ご, word, speech, language] 92 | 読: [よみ, read] 93 | 買: [かい, buy] 94 | 足: [あし, leg, foot, be sufficient, counter for pairs of footwear] 95 | 車: [くるま, car] 96 | 週: [しゅう, week] 97 | 道: [みち, road-way, street, district, journey, course, moral, teachings] 98 | 金: [きん, gold] 99 | 長: [なが, long, leader] 100 | 間: [ま, interval, space] 101 | 雨: [あめ, rain] 102 | 電: [でん, electricity] 103 | 食: [しょく, eat, food] 104 | 飲: [いん, drink, smoke, take] 105 | 駅: [えき, station] 106 | 高: [こう, tall, high, expensive] 107 | 魚: [さかな, fish] 108 | -------------------------------------------------------------------------------- /cards/en/japanese-katakana-keyboard-mappings.yml: -------------------------------------------------------------------------------- 1 | --- # https://en.wikibooks.org/wiki/Japanese/Kana_chart 2 | Japanese: 3 | Katakana Keyboard Mappings: 4 | mapping: { Katakana: Romaji } 5 | ア: a 6 | イ: [i, yi] 7 | ウ: [u, wu] 8 | エ: e 9 | オ: o 10 | ー: '-' 11 | ァ: xa 12 | ィ: xi 13 | ゥ: xu 14 | ェ: xe 15 | ォ: xo 16 | カ: [ka, ca] 17 | キ: ki 18 | ク: [ku, cu] 19 | ケ: ke 20 | コ: [ko, co] 21 | ガ: ga 22 | ギ: gi 23 | グ: gu 24 | ゲ: ge 25 | ゴ: go 26 | サ: sa 27 | シ: [si, shi] 28 | ス: su 29 | セ: se 30 | ソ: so 31 | ザ: za 32 | ジ: [zi, ji] 33 | ズ: zu 34 | ゼ: ze 35 | ゾ: zo 36 | ジャ: [ja, zya, jya] 37 | ジュ: [ju, zyu, jyu] 38 | ジェ: [je, zye, jye] 39 | ジョ: [jo, zyo, jyo] 40 | タ: ta 41 | チ: [ti, chi] 42 | ツ: [tu, tsu] 43 | テ: te 44 | ト: to 45 | ダ: da 46 | ヂ: di 47 | ヅ: du 48 | デ: de 49 | ド: do 50 | ナ: na 51 | ニ: ni 52 | ヌ: nu 53 | ネ: ne 54 | ノ: 'no' 55 | ハ: ha 56 | ヒ: hi 57 | フ: [hu, fu] 58 | ヘ: he 59 | ホ: ho 60 | バ: ba 61 | ビ: bi 62 | ブ: bu 63 | ベ: be 64 | ボ: bo 65 | パ: pa 66 | ピ: pi 67 | プ: pu 68 | ペ: pe 69 | ポ: po 70 | ヴァ: va 71 | ヴィ: vi 72 | ヴ: vu 73 | ヴェ: ve 74 | ヴォ: vo 75 | ファ: fa 76 | フィ: fi 77 | フェ: fe 78 | フォ: fo 79 | マ: ma 80 | ミ: mi 81 | ム: mu 82 | メ: me 83 | モ: mo 84 | ヤ: ya 85 | ユ: yu 86 | イェ: ye 87 | ヨ: yo 88 | ラ: [ra, la] 89 | リ: [ri, li] 90 | ル: [ru, lu] 91 | レ: [re, le] 92 | ロ: [ro, lo] 93 | ワ: wa 94 | ウィ: wi 95 | ウェ: we 96 | ヲ: wo 97 | ン: [nn, n] 98 | ヵ: xka 99 | ヶ: xke 100 | ヮ: xwa 101 | ッ: xtsu 102 | ャ: xya 103 | ュ: xyu 104 | ョ: xyo 105 | キャ: kya 106 | キィ: kyi 107 | キュ: kyu 108 | キェ: kye 109 | キョ: kyo 110 | ギャ: gya 111 | ギィ: gyi 112 | ギュ: gyu 113 | ギェ: gye 114 | ギョ: gyo 115 | シャ: [sya, sha] 116 | シィ: syi 117 | シュ: [syu, shu] 118 | シェ: [sye, she] 119 | ショ: [syo, sho] 120 | ジィ: [zyi, jyi] 121 | チャ: [tya, cya, cha] 122 | チィ: [tyi, cyi] 123 | チュ: [tyu, cyu, chu] 124 | チェ: [tye, cye, che] 125 | チョ: [tyo, cyo, cho] 126 | トァ: twa 127 | トィ: twi 128 | トゥ: twu 129 | トェ: twe 130 | トォ: two 131 | ドァ: dwa 132 | ドィ: dwi 133 | ドゥ: dwu 134 | ドェ: dwe 135 | ドォ: dwo 136 | テャ: tha 137 | ティ: thi 138 | テュ: thu 139 | テェ: the 140 | テョ: tho 141 | ヂャ: dya 142 | ヂィ: dyi 143 | ヂュ: dyu 144 | ヂェ: dye 145 | ヂョ: dyo 146 | デャ: dha 147 | ディ: dhi 148 | デュ: dhu 149 | デェ: dhe 150 | デョ: dho 151 | ニャ: nya 152 | ニィ: nyi 153 | ニュ: nyu 154 | ニェ: nye 155 | ニョ: nyo 156 | ヒャ: hya 157 | ヒィ: hyi 158 | ヒュ: hyu 159 | ヒェ: hye 160 | ヒョ: hyo 161 | ビャ: bya 162 | ビィ: byi 163 | ビュ: byu 164 | ビェ: bye 165 | ビョ: byo 166 | ピャ: pya 167 | ピィ: pyi 168 | ピュ: pyu 169 | ピェ: pye 170 | ピョ: pyo 171 | ミャ: mya 172 | ミィ: myi 173 | ミュ: myu 174 | ミェ: mye 175 | ミョ: myo 176 | リャ: [rya, lya] 177 | リィ: [ryi, lyi] 178 | リュ: [ryu, lyu] 179 | リェ: [rye, lye] 180 | リョ: [ryo, lyo] 181 | -------------------------------------------------------------------------------- /cards/en/japanese-katakana.yml: -------------------------------------------------------------------------------- 1 | --- # https://en.wikibooks.org/wiki/Japanese/Kana_chart 2 | Japanese: 3 | Katakana: 4 | mapping: { Katakana: Romaji } 5 | ア: a 6 | イ: i 7 | ウ: u 8 | エ: e 9 | オ: o 10 | ー: '-' 11 | カ: ka 12 | キ: ki 13 | ク: ku 14 | ケ: ke 15 | コ: ko 16 | ガ: ga 17 | ギ: gi 18 | グ: gu 19 | ゲ: ge 20 | ゴ: go 21 | サ: sa 22 | ス: su 23 | セ: se 24 | ソ: so 25 | ザ: za 26 | ズ: zu 27 | ゼ: ze 28 | ゾ: zo 29 | ジャ: ja 30 | ジ: ji 31 | ジュ: ju 32 | ジョ: jo 33 | タ: ta 34 | テ: te 35 | ト: to 36 | ダ: da 37 | デ: de 38 | ド: do 39 | ナ: na 40 | ニ: ni 41 | ヌ: nu 42 | ネ: ne 43 | ノ: 'no' 44 | ハ: ha 45 | ヒ: hi 46 | ヘ: he 47 | ホ: ho 48 | バ: ba 49 | ビ: bi 50 | ブ: bu 51 | ベ: be 52 | ボ: bo 53 | パ: pa 54 | ピ: pi 55 | プ: pu 56 | ペ: pe 57 | ポ: po 58 | フ: fu 59 | マ: ma 60 | ミ: mi 61 | ム: mu 62 | メ: me 63 | モ: mo 64 | ヤ: ya 65 | ユ: yu 66 | ヨ: yo 67 | ラ: ra 68 | リ: ri 69 | ル: ru 70 | レ: re 71 | ロ: ro 72 | ワ: wa 73 | ヲ: wo 74 | ン: n 75 | ツ: tsu 76 | キャ: kya 77 | キュ: kyu 78 | キョ: kyo 79 | ギャ: gya 80 | ギュ: gyu 81 | ギョ: gyo 82 | シャ: sha 83 | シ: shi 84 | シュ: shu 85 | ショ: sho 86 | チャ: cha 87 | チ: chi 88 | チュ: chu 89 | チョ: cho 90 | ニャ: nya 91 | ニュ: nyu 92 | ニョ: nyo 93 | ビャ: bya 94 | ビュ: byu 95 | ビョ: byo 96 | ピャ: pya 97 | ピュ: pyu 98 | ピョ: pyo 99 | ミャ: mya 100 | ミュ: myu 101 | ミョ: myo 102 | リャ: rya 103 | リュ: ryu 104 | リョ: ryo 105 | ヂ: ji 106 | ヅ: zu 107 | ヂャ: ja 108 | ヂュ: ju 109 | ヂョ: jo 110 | -------------------------------------------------------------------------------- /language_cards.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'language_cards/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'language_cards' 8 | spec.version = LanguageCards::VERSION 9 | spec.authors = ['Daniel P. Clark'] 10 | spec.email = ['6ftdan@gmail.com'] 11 | 12 | spec.summary = %q{Flashcard game for language learning.} 13 | spec.description = %q{Flashcard game for language learning. Make your own cards or translations as well.} 14 | spec.homepage = 'http://github.com/danielpclark/language_cards' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | 21 | spec.executables = ['language_cards'] 22 | spec.require_paths = ['lib','cards'] 23 | 24 | spec.add_dependency 'highline', '~> 1.7' 25 | spec.add_dependency 'i18n', '~> 0.7' 26 | spec.add_dependency 'slop', '~> 4.6' 27 | spec.add_development_dependency 'bundler', '~> 1.13' 28 | spec.add_development_dependency 'rake', '~> 12.3' 29 | spec.add_development_dependency 'minitest', '~> 5.10' 30 | end 31 | -------------------------------------------------------------------------------- /lib/language_cards.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/version' 2 | require 'language_cards/defaults' 3 | require 'language_cards/yaml_loader' 4 | require 'language_cards/user_interface' 5 | require 'language_cards/menu_builder' 6 | 7 | ## 8 | # TODO: 9 | # * Implement score-keeper 10 | # * Race against the clock 11 | # * Weighted random for better learning 12 | 13 | module LanguageCards 14 | def self.start 15 | yaml = YAMLLoader.new.load 16 | menu = menu_builder(yaml) 17 | UserInterface.new(menu).start 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/language_cards/card_set_builder.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/models/card' 2 | 3 | module LanguageCards 4 | module CardSetBuilder 5 | def card_set_builder(translation_cards = {}) 6 | translation_cards.each_with_object([]) do |(key, value), memo| 7 | memo << Card.new(key, value) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/language_cards/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module LanguageCards 4 | module Controllers 5 | class ApplicationController 6 | include Helpers::ViewHelper 7 | 8 | def initialize(opts = {}) 9 | @opts = opts 10 | end 11 | 12 | def render(_binding) 13 | view = ERB.new IO.read File.expand_path("../view/#{snake name}.erb", __dir__) 14 | 15 | view.result(_binding) 16 | end 17 | 18 | private 19 | attr_reader :opts 20 | def name 21 | self.class.name.split('::').last 22 | end 23 | 24 | def errors 25 | Array(opts[:errors]) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/language_cards/controllers/game.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/controllers/application_controller' 2 | 3 | module LanguageCards 4 | module Controllers 5 | class Game < ApplicationController 6 | include Helpers::GameHelper 7 | 8 | def render(correct:, incorrect:, title:, timer:, last:) 9 | _score = t('Game.ScoreMenu.Score') + ": %0.2d%%" % calc_score(correct, incorrect) 10 | _timer = [((t('Timer.Timer') + ": " + timer.ha) if timer.time?), nil, timer.h] 11 | _mexit = t 'Menu.Exit' 12 | 13 | super(binding) 14 | end 15 | 16 | def process(cards) 17 | ic = struct_data.new(cards) 18 | ic.get_input 19 | { 20 | correct: ic.valid?, 21 | last: ic.valid? ? ic.correct_msg : ic.incorrect_msg 22 | } 23 | end 24 | 25 | def struct_data 26 | Struct.new(:game) do 27 | def input 28 | @input 29 | end 30 | 31 | def get_input 32 | @input ||= CLI.ask("#{I18n.t('Game.TypeThis')}: #{display}") 33 | end 34 | 35 | def card 36 | @card ||= game.sample.current 37 | end 38 | 39 | def display 40 | "#{card}" 41 | end 42 | 43 | def mode 44 | game.mode 45 | end 46 | 47 | def expected 48 | case mode 49 | when :translate 50 | card.translation 51 | when :typing_practice 52 | "#{card}" 53 | else 54 | raise "Invalid mode in Game Controller!" 55 | end 56 | end 57 | 58 | def correct_msg 59 | "#{I18n.t('Game.Correct')} #{input} = #{display}" 60 | end 61 | 62 | def incorrect_msg 63 | output = "#{I18n.t('Game.Incorrect')} #{input} != #{display}" 64 | output << " #{I18n.t('Game.Its')} #{expected.join(', ')}" if mode == :translate 65 | output 66 | end 67 | 68 | def valid? 69 | game.match? input 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/language_cards/controllers/main_menu.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/controllers/application_controller' 2 | 3 | module LanguageCards 4 | module Controllers 5 | class MainMenu < ApplicationController 6 | def render(courses:, mode:) 7 | _title = t 'Menu.Title' 8 | _select = t 'Menu.Choose' 9 | _mode = t('Menu.GameMode') + case mode.peek 10 | when :translate then t 'Menu.ModeTranslate' 11 | when :typing_practice then t 'Menu.ModeTyping' 12 | end 13 | _toggle = "m: " + t('Menu.ToggleGameMode') 14 | _courses = courses.each.with_index.map {|item,index| "#{index + 1}: #{item}" } 15 | _mexit = t 'Menu.Exit' 16 | 17 | super(binding) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/language_cards/defaults.rb: -------------------------------------------------------------------------------- 1 | require 'highline' 2 | require 'slop' 3 | require 'i18n' 4 | 5 | module LanguageCards 6 | OPTS = Slop.parse do |args| 7 | args.string '-l', '--language', 'language (default: en)', default: 'en' 8 | end 9 | 10 | CARD_LANGUAGE = OPTS[:language] 11 | 12 | module ESC 13 | CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J") 14 | end 15 | 16 | CLI = HighLine.new 17 | JOIN = " - " 18 | 19 | SUBMENUWIDTH = 60 20 | 21 | ::I18n.config.available_locales = :en 22 | ::I18n.load_path = Dir[File.join(File.expand_path(File.join('..','..'), __dir__), 'locales', '*.yml')] 23 | ::I18n.load_path += Dir[File.join(File.expand_path(ENV['HOME']), '.language_cards', 'locales', '*.yml')] if ENV['HOME'] 24 | end 25 | -------------------------------------------------------------------------------- /lib/language_cards/helpers/game_helper.rb: -------------------------------------------------------------------------------- 1 | module LanguageCards 2 | module Helpers 3 | module GameHelper 4 | def calc_score c, i # correct, incorrect 5 | (0.001+c.to_i)*100.0/(c.to_i+i.to_i+0.001)*1.0 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/language_cards/helpers/view_helper.rb: -------------------------------------------------------------------------------- 1 | module LanguageCards 2 | module Helpers 3 | module ViewHelper 4 | def divider 5 | '~' * SUBMENUWIDTH 6 | end 7 | 8 | def t str 9 | I18n.t str 10 | end 11 | 12 | def draw left=nil, center=nil, right=nil 13 | width = SUBMENUWIDTH 14 | str = left.to_s 15 | str = str + center.to_s.rjust(width/2 - str.length + center.to_s.length/2) 16 | str + right.to_s.rjust(width - str.length) 17 | end 18 | 19 | def clear 20 | printf ::LanguageCards::ESC::CLEAR 21 | end 22 | 23 | def humanize string 24 | "#{string}".split('_').map(&:capitalize).join(' ') 25 | end 26 | 27 | def snake string 28 | "#{string}".gsub(/(.)([A-Z])/, '\1_\2').downcase 29 | end 30 | 31 | def wordwrap words 32 | "#{words}".gsub(/(.{1,#{SUBMENUWIDTH - 7}})(\s+|\Z)/, "\\1\n\t").strip 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/language_cards/menu_builder.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/menu_node' 2 | 3 | module LanguageCards 4 | def self.menu_builder(cards_yaml) 5 | cards_yaml.each_with_object([]) do |(language, values), memo| 6 | values.each do |category_with_card_set| 7 | memo << MenuNode.new(language, category_with_card_set) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/language_cards/menu_node.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/models/card_set' 2 | module LanguageCards 3 | class MenuNode 4 | def initialize name, child 5 | @name = name 6 | 7 | if child.is_a?(Hash) and child.has_key?("mapping") 8 | @mapping = child.delete("mapping") # Extra unused data for the moment 9 | @child = CardSet.new(child) 10 | else 11 | @child = MenuNode.new(*child) 12 | end 13 | end 14 | 15 | def title(fmt = JOIN, rng = 0..-1) 16 | label[rng].delete_if(&:empty?).join(fmt) 17 | end 18 | 19 | # @return < Game> 20 | def game(mode) 21 | child.game(mode) 22 | end 23 | 24 | # This is the preferred method for the view as this object shouldn't 25 | # care about how it should be displayed in the view. 26 | # @return Array 27 | def label 28 | [@name].push(*child.label) 29 | end 30 | 31 | def to_s 32 | label.delete_if(&:empty?).join(JOIN) 33 | end 34 | 35 | private 36 | attr_reader :name, :child 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/language_cards/models/card.rb: -------------------------------------------------------------------------------- 1 | 2 | module LanguageCards 3 | class Card 4 | attr_reader :translation 5 | def initialize card, translation 6 | @card = card 7 | @translation = Array(translation) 8 | end 9 | 10 | def display 11 | @card 12 | end 13 | 14 | def to_s 15 | @card 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/language_cards/models/card_set.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/models/card' 2 | require 'language_cards/card_set_builder' 3 | require 'language_cards/modes/typing_practice' 4 | require 'language_cards/modes/translate' 5 | 6 | module LanguageCards 7 | class CardSet 8 | include CardSetBuilder 9 | attr_reader :cards 10 | def initialize(card_hash) 11 | @cards = card_set_builder(card_hash) 12 | end 13 | 14 | def game(mode) 15 | Modes.public_send mode, self 16 | rescue NoMethodError 17 | raise InvalidGameMode, "Invalid Game Mode!" 18 | end 19 | 20 | # So as to not interfere with menu naming as this is not meant to 21 | # be displayed as a string. 22 | def to_s 23 | "" 24 | end 25 | 26 | def label 27 | [] 28 | end 29 | 30 | private 31 | class InvalidGameMode < StandardError; end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/language_cards/modes/game.rb: -------------------------------------------------------------------------------- 1 | 2 | module LanguageCards 3 | module Modes 4 | class Game 5 | def initialize card_set 6 | @card_set = card_set.cards 7 | @index = 0 8 | @current = nil 9 | end 10 | 11 | def current 12 | @current or raise "Current flash card not yet set!" 13 | end 14 | 15 | # @return Grapheme Returns a random grapheme 16 | def sample 17 | @current = @card_set.sample 18 | self 19 | end 20 | 21 | # Iterator for cycling through all translations sequentially. 22 | # @return Grapheme Returns a random grapheme 23 | def next 24 | value = @card_set[@index % @card_set.length] 25 | @index += 1 26 | @current = value 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/language_cards/modes/translate.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/modes/game' 2 | module LanguageCards 3 | module Modes 4 | def self.translate card_set 5 | Translate.new card_set 6 | end 7 | 8 | class Translate < Game 9 | def match? input 10 | current.translation.any? {|value| value == input } 11 | end 12 | 13 | def mode 14 | :translate 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/language_cards/modes/typing_practice.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/modes/game' 2 | module LanguageCards 3 | module Modes 4 | def self.typing_practice card_set 5 | TypingPractice.new card_set 6 | end 7 | 8 | class TypingPractice < Game 9 | def match? input 10 | "#{current}" == input 11 | end 12 | 13 | def mode 14 | :typing_practice 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/language_cards/timer.rb: -------------------------------------------------------------------------------- 1 | 2 | module LanguageCards 3 | class Timer 4 | def initialize 5 | @stamps = [] 6 | @mark = nil 7 | end 8 | 9 | def mark 10 | if @mark 11 | @stamps << -(@mark - (@mark = Time.now)) 12 | else 13 | @mark = Time.now 14 | end 15 | end 16 | 17 | def time? 18 | !times.empty? 19 | end 20 | 21 | def h # human 22 | "%02d:%02d:%02d" % [total/3600%24, total/60%60, total%60] 23 | end 24 | 25 | def average 26 | total.fdiv(times.size) 27 | end 28 | 29 | def ha # human average 30 | "%0.2f #{I18n.t('Timer.AverageSeconds')}" % average rescue "" 31 | end 32 | 33 | def times 34 | @stamps 35 | end 36 | 37 | def last 38 | @stamps.last 39 | end 40 | 41 | def total 42 | @stamps.inject(:+) || 0 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/language_cards/user_interface.rb: -------------------------------------------------------------------------------- 1 | require 'language_cards/timer' 2 | require 'language_cards/helpers/view_helper' 3 | require 'language_cards/helpers/game_helper' 4 | require 'language_cards/controllers/main_menu' 5 | require 'language_cards/controllers/game' 6 | 7 | module LanguageCards 8 | class UserInterface 9 | include Helpers::ViewHelper 10 | include Controllers 11 | def initialize menu_items 12 | @menu_items = menu_items 13 | @courses = process_courses(menu_items) 14 | @mode = [:translate, :typing_practice].cycle 15 | end 16 | 17 | def start 18 | unless ENV['SKIP_SPLASH'] 19 | clear 20 | CLI.say SPLASH_SCREEN 21 | sleep 2 22 | end 23 | 24 | begin 25 | loop do 26 | clear 27 | 28 | CLI.say MainMenu.new(opts).render courses: courses, mode: mode 29 | 30 | value = CLI.ask("") 31 | 32 | next mode.next if value =~ /\Am\z/i 33 | value = value.to_i - 1 rescue next 34 | 35 | last = nil 36 | if (0..courses.length-1).include? value 37 | 38 | collection = menu_items[value] # MenuNode 39 | title = "#{collection.title} (#{humanize mode.peek})" 40 | collection = collection.game(mode.peek) # Mode < Game 41 | 42 | game = Game.new(opts) 43 | timer = Timer.new 44 | begin # Game Loop 45 | loop do 46 | clear 47 | timer.mark 48 | CLI.say game.render correct: correct, 49 | incorrect: incorrect, 50 | title: title, 51 | timer: timer, 52 | last: last 53 | result = game.process(collection) 54 | result[:correct] ? correct! : incorrect! 55 | last = result[:last] 56 | end 57 | rescue SystemExit, Interrupt 58 | end 59 | end 60 | end 61 | 62 | rescue SystemExit, Interrupt 63 | end 64 | end 65 | 66 | private 67 | attr_reader :mode, :menu_items, :correct, :incorrect, :courses 68 | def opts 69 | @opts ||= {} 70 | end 71 | 72 | def correct! 73 | @correct = @correct.to_i + 1 74 | end 75 | 76 | def incorrect! 77 | @incorrect = @incorrect.to_i + 1 78 | end 79 | 80 | def process_courses(menu_items) 81 | courses = menu_items.flat_map {|i| i.label.join(JOIN) } 82 | 83 | if courses.empty? 84 | opts[:errors] = ["No Flash Cards found for language: #{CARD_LANGUAGE}"] 85 | end 86 | 87 | courses 88 | end 89 | end 90 | end 91 | 92 | 93 | SPLASH_SCREEN = %q( 94 | _ _ __ _ ____ _ _ _ ____ _______ 95 | | | / \ | \ | | / __ \ | | | | / \ / __ \ | _____| 96 | | | / _ \ | \ | | / / \_\ | | | | / _ \ / / \_\ | | 97 | | | / /_\ \ | |\ \| || | ___ | | | | / /_\ \ | | ___ | ^‒‒‒v 98 | | | / _____ \ | | \ \ || | |_ || | | | / _____ \ | | |_ || .‒‒‒^ 99 | | |____ / / \ \ | | \ | \ \__/ | \ \_/ / / / \ \ \ \__/ || |_____ 100 | |______|/_/ \_\|_| \_| \____/_| \___/ /_/ \_\ \____/_||_______| 101 | 102 | 103 | ____ _ _____ _____ _____ 104 | / __ \ / \ | __ \ | __ \ / ___/ 105 | / / \_\ / _ \ | | \ \ | | \ \ / /__ 106 | | | / /_\ \ | |__/ / | | | | \___ \ 107 | | | _ / _____ \ | __ < | | | | \ \ 108 | \ \__/ / / / \ \ | | \ \ | |__/ / ___/ / 109 | \____/ /_/ \_\|_| \_\ |_____/ /____/ 110 | 111 | 112 | 113 | 114 | by Daniel P. Clark 115 | 116 | @6ftdan 117 | ) 118 | -------------------------------------------------------------------------------- /lib/language_cards/version.rb: -------------------------------------------------------------------------------- 1 | module LanguageCards 2 | VERSION = "0.3.2" 3 | end 4 | -------------------------------------------------------------------------------- /lib/language_cards/view/game.erb: -------------------------------------------------------------------------------- 1 | <%= divider %> 2 | <%= draw nil, title %> 3 | <%= divider %> 4 | <%= draw(*_timer) %> 5 | <%= divider %> 6 | <%= draw (if correct || incorrect then _score end), nil, _mexit %> 7 | 8 | <%= wordwrap last %> 9 | <%= divider %> 10 | -------------------------------------------------------------------------------- /lib/language_cards/view/main_menu.erb: -------------------------------------------------------------------------------- 1 | <%= divider %> 2 | <%= draw(_title, nil, 'v' + VERSION) %> 3 | <%= divider %> 4 | <%= draw(_select, nil, _mode) %> 5 | <% (_courses.empty? ? errors : _courses).each do |course| %> 6 | <%= course.chomp %><% end %> 7 | 8 | <%= draw(_mexit, nil, _toggle) %> 9 | <%= divider %> 10 | -------------------------------------------------------------------------------- /lib/language_cards/yaml_loader.rb: -------------------------------------------------------------------------------- 1 | module LanguageCards 2 | class YAMLLoader 3 | def initialize 4 | @language = CARD_LANGUAGE 5 | end 6 | 7 | def load 8 | cards = {} 9 | 10 | cards_yaml.each do |c| 11 | next unless yaml_data = YAML.load(File.open(c).read) 12 | for language in yaml_data.keys do 13 | # Merges sub-items for languages 14 | if cards.has_key? language 15 | cards[language] = Hash( cards[language] ).merge( Hash(yaml_data[language]) ) 16 | else 17 | # Merges in new top scope languages 18 | cards.merge!({ language => yaml_data[language] }) 19 | end 20 | end 21 | end 22 | 23 | cards 24 | end 25 | 26 | private 27 | attr_reader :language 28 | 29 | def application_path 30 | File.expand_path(File.join('..','..'), __dir__) 31 | end 32 | 33 | def cards_yaml 34 | application_path_cards_yaml + home_path_cards_yaml 35 | end 36 | 37 | def application_path_cards_yaml 38 | Dir[File.join(application_path, 'cards', language, '*.yml')] 39 | end 40 | 41 | def home_path_cards_yaml 42 | if ENV['HOME'] 43 | Dir[File.join(ENV['HOME'], '.language_cards', 'cards', language, '*.yml')] 44 | else 45 | [] 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | Menu: 3 | Title: Language Cards by @6ftdan 4 | Choose: Select an option 5 | GameMode: "GAME MODE: " 6 | ToggleGameMode: Change Mode 7 | ModeTranslate: Translate 8 | ModeTyping: Typing 9 | Exit: Press CTRL-C to quit. 10 | Game: 11 | TypeThis: Type the following in 12 | Correct: Correct! 13 | Incorrect: Wrong! 14 | Its: it's 15 | ScoreMenu: 16 | Score: Your score is 17 | Timer: 18 | Timer: Timer 19 | AverageSeconds: second average 20 | Errors: 21 | UpperCollection: Upper level collection container.\nYou need to use a deeper collection for cards. 22 | InvalidMapping: > 23 | Invalid mapping in YAML definition. 24 | Their needs to be exactly two entries. 25 | * one with an "index" key 26 | * and one with one language name as the key and the other as a value 27 | Fix your language mapping. 28 | EmptyCollection: Invalid option on empty card collection! 29 | MappingNotFound: The key for the mapping you've provided isn't in the collection! 30 | LanguageName: 31 | Japanese: Japanese 32 | Romaji: Romaji 33 | Katakana: Katakana 34 | Katakana Diacritics: Katakana Diacritics 35 | Katakana Keyboard Mappings: Katakana Key Mappings 36 | Hiragana: Hiragana 37 | Hiragana Diacritics: Hiragana Diacritics 38 | -------------------------------------------------------------------------------- /test/card_set_builder_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | include LanguageCards 4 | 5 | class CardSetBuilderTest < Minitest::Test 6 | include CardSetBuilder 7 | attr_reader :builder 8 | def setup 9 | @builder = card_set_builder( 10 | {'く' => 'ku'} 11 | ) 12 | end 13 | 14 | def test_defaults 15 | assert_kind_of Array, builder 16 | assert_kind_of Card, builder.first 17 | assert_equal 'ku', builder.first.translation.first 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/card_set_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | include LanguageCards 4 | 5 | class CardSetTest < Minitest::Test 6 | attr_reader :card_set 7 | def setup 8 | @card_set = CardSet.new({'く' => 'ku'}) 9 | end 10 | 11 | def test_creates_collection 12 | assert card_set.respond_to? :game 13 | assert card_set.respond_to? :cards 14 | assert card_set.cards.first.respond_to? :translation 15 | end 16 | 17 | def test_modes 18 | card = card_set.game(:translate) 19 | card.sample 20 | assert card.match? 'ku' 21 | 22 | card = card_set.game(:typing_practice) 23 | card.sample 24 | assert card.match? 'く' 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/grapheme_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | include LanguageCards 4 | 5 | class CardTest < Minitest::Test 6 | attr_reader :card 7 | def setup 8 | @card = Card.new('く', 'ku') 9 | end 10 | 11 | def test_defaults 12 | assert_equal 'く', "#{card}" 13 | assert_equal 'ku', *card.translation 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/language_cards_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LanguageCardsTest < Minitest::Test 4 | include LanguageCards 5 | def test_that_it_has_a_version_number 6 | refute_nil VERSION 7 | end 8 | 9 | def test_i18n_loads_translation 10 | refute_empty ::I18n.load_path 11 | end 12 | 13 | def test_clear_is_a_valid_clear_string 14 | assert_kind_of String, ESC::CLEAR 15 | refute ESC::CLEAR.empty? 16 | end 17 | 18 | def test_mkmf_log_file_avoided 19 | refute File.exist?(File.join('..', 'mkmf.log')) 20 | refute File.exist?('mkmf.log') 21 | end 22 | 23 | def test_cards_load 24 | cc = LanguageCards.menu_builder YAMLLoader.new.load 25 | assert_kind_of MenuNode, cc.first 26 | assert cc.detect {|i| /Japanese/ === "#{i}"} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/mappings_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MappingsTest < Minitest::Test 4 | def test_failed_mapping 5 | assert_raises {LanguageCards::Mappings.new([{'a' => 'b', 'c' => 'd', 'incoming' => [:k, :v]}])} 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/support.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | def self.included(base) 3 | base.class_exec do 4 | ## 5 | # NOTE: keys must use valid I18n translations for testing 6 | # 7 | def mapping_key; "Romaji => Hiragana" end 8 | def map; [{"Romaji" => "Hiragana", "index" => [:k, :v]}] end 9 | def mapping; LanguageCards::Mappings.new(map, collection) end 10 | def collection 11 | c = LanguageCards::CardCollection.new({"mapping" => map, "a" => "ア"}) 12 | c.select_collection(mapping_key) 13 | c 14 | end 15 | 16 | def mapping_key2; "Hiragana => Hiragana" end 17 | def map2; [{"Hiragana" => "Hiragana", "index" => [:v, :v]}] end 18 | def mapping2; LanguageCards::Mappings.new(map2, collection2) end 19 | def collection2 20 | c = LanguageCards::CardCollection.new({"mapping" => map2, "a" => "ア"}) 21 | c.select_collection(mapping_key2) 22 | c 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'simplecov' 3 | SimpleCov.start 4 | require 'open3' 5 | require 'language_cards' 6 | require 'minitest/autorun' 7 | require 'support' 8 | 9 | class Minitest::Test 10 | include Support 11 | attr_reader :out, :err 12 | def sys_exec(cmd) 13 | Open3.popen3(cmd.to_s) do |stdin, stdout, stderr, wait_thr| 14 | yield stdin, stdout, wait_thr if block_given? 15 | stdin.close 16 | 17 | @exitstatus = wait_thr && wait_thr.value.exitstatus 18 | @out = Thread.new { stdout.read }.value.strip 19 | @err = Thread.new { stderr.read }.value.strip 20 | end 21 | 22 | (@all_output ||= String.new) << [ 23 | "$ #{cmd.to_s.strip}", 24 | out, 25 | err, 26 | @exitstatus ? "# $? => #{@exitstatus}" : "", 27 | "\n", 28 | ].reject(&:empty?).join("\n") 29 | 30 | @out 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/timer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'language_cards/timer' 3 | 4 | class TimerTest < Minitest::Test 5 | def test_timer_returns_float_greater_than_zero 6 | t = LanguageCards::Timer.new 7 | t.mark 8 | t.mark 9 | assert_includes 0..2, t.times.first 10 | assert_includes 0..2, t.last 11 | assert_includes 0..2, t.total 12 | assert_kind_of Float, t.total 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/user_interface_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserInterfaceTest < Minitest::Test 4 | def test_menu_contains_parts 5 | mm = LanguageCards::Controllers::MainMenu.new.render( 6 | courses: ["Japanese"], 7 | mode: [:translate].cycle 8 | ) 9 | assert (/#{I18n.t('Menu.Title')}/ === mm) 10 | assert (/1: Japanese/ === mm) 11 | assert (/#{I18n.t('Menu.Exit')}/ === mm) 12 | end 13 | 14 | def test_game_contains_parts 15 | sm = LanguageCards::Controllers::Game.new.render( 16 | correct: 1, 17 | incorrect: 2, 18 | title: 'Hiragana', 19 | timer: LanguageCards::Timer.new, 20 | last: nil 21 | ) 22 | assert (/#{I18n.t('Game.ScoreMenu.Score')}/ === sm) 23 | assert (/#{I18n.t('Menu.Exit')}/ === sm) 24 | end 25 | 26 | def test_clear_terminal_code_is_correct 27 | assert_equal "\e[3J\e[H\e[2J", LanguageCards::ESC::CLEAR 28 | end 29 | 30 | def test_main_menu_in_application_load 31 | cmd = "SKIP_FLASH=1 #{File.expand_path('../bin/language_cards', __dir__)}" 32 | result = sys_exec(cmd){|i,*| i.puts "1"; i.puts "wa"} 33 | assert_match(I18n.t('Menu.Title'), result) 34 | assert_match(LanguageCards::VERSION, result) 35 | assert_match(I18n.t('LanguageName.Japanese'), result) 36 | assert_match(I18n.t('Game.ScoreMenu.Score'), result) 37 | assert_match(I18n.t('Timer.Timer'), result) 38 | assert_match(I18n.t('Timer.AverageSeconds'), result) 39 | assert_match(/00:00:00/, result) 40 | assert_match(I18n.t('Menu.Exit'), result) 41 | assert_match(LanguageCards::ESC::CLEAR, result) 42 | end unless Gem.win_platform? 43 | end 44 | --------------------------------------------------------------------------------