百人一首
Table of Contents
百人一首
前回のREADMEに刺激されて、とんでもない方面に首をつっこんでみる。 春の陽気で気でも狂ったかと言うなかれ、ちゃんと計算ずくです。
小倉百人一首 読み上げ 序歌+ランダム(1~100) YouTube 朗々とした発声が素晴しい。序歌ってのは、これから競技カルタを始めますよって合図。 いわば、ヨーイ・ドンのヨーイに相当する。
真っ先に思い浮かぶ「百人一首」の歌ランキング 1位から10位 何でもランキングしちゃうのが日本人の習性です。
あなたの好きな百人一首和歌2021結果発表 宇都宮市の公式ページから。昔、 宇都宮地方を統治してた宇都宮某は、謀反の疑いをかけられて出家。京都に出て 隠遁生活を始める。庵に彩って事で、藤原定家に和歌を百首選んでもらって短冊を 飾った。ってな事で、百人一首のゆかりの地を名乗っているそうな。
五七五七七の五七五を上の句、これを読み始めると、対応する下の句(七七)を、す速く 取るってのが競技カルタの基本のルールだ。まずは、百首の暗記だな。
ちょいと分析
百人一首 簡単に暗記について これに相当する暗記アプリをvlangで作成してみたいのだけど、その前に敵を知って おこう。
解説によると、一字決まりの和歌は、「むすめふさほせ」の七首のみ。二字決まりの札は四十二首、三字決まりの札は三十七首 らしい。
日本語なのに単語区切りがスペースになってる、一見英語文みたいなのを用意した。 左の数字は歌番と呼ばれる、いわばIDだな。歌によっては微妙に余り字があって おもしろい。上の句で並び替えをやってみた。
sakae@lu:h100$ sort -k 2 genko.txt 43 あいみての のちのこころに くらぶれば むかしはものを おもわざりけり 44 あうことの たえてしなくば なかなかに ひとをもみをも うらみざらまし : 11 わたのはら やそしまかけて こぎいでぬと ひとにはつげよ あまのつりぶね 20 わびぬれば いまはたおなじ なにわなる みをつくしても あわんとぞおもう
そして、あさ で始まる歌を検索。5文字まで一致してて、6文字目でやっと一意に 決定される歌が有るんですね。頭の中で補完して、対応する下の句を抽出するって 脳負荷が結構高そうだ。
sakae@lu:h100$ grep '[0-9] あさ' genko.txt 31 あさぼらけ ありあけのつきと みるまでに よしののさとに ふれるしらゆき 39 あさじうの おののしのはら しのぶれど あまりてなどか ひとのこいしき 64 あさぼらけ うじのかわぎり たえだえに あらわれわたる せぜのあじろぎ
今度は、冒頭文字の頻度分析。これが暗号解読の重要なヒントになりますから、 こういうのは必須です。
sakae@lu:h100$ cat genko.txt | cut -d' ' -f2 | cut -b1-3 | sort | uniq -c | sort -nr 17 あ 8 な 7 わ 6 た 6 こ 6 お 5 み 4 よ 4 や 4 は 4 か 3 ひ 3 ち 3 き 3 い 2 ゆ 2 も 2 つ 2 し 2 う 1 め 1 む 1 ほ 1 ふ 1 せ 1 す 1 さ
regexの試用
まずは、日本語相手に、上記の長いパイプをvlangで実現してみたい。 正直、日本語の扱かいは今迄避けていたけど、今回は ひらがな だけが相手なんで 何とかなるだろうと思った次第。
vlib/builtinを探れば使えそうなのが沢山見付かるだろうけど、 vlibに正規表現のモジュールが有ったので、そろりそろりと試用してみる。
import regex fn main() { text := 'abcdefg' mut re := regex.regex_opt(r'b.*d')! start, end := re.find(text) println(start) println(end) println('${text[start..end]}') }
検索対象(text)を用意し、正規表現(b.*d)で検索。結果は、対象文字列配列の インデックス番号で返ってくる。そのインデックス番号で、配列をスライスして 表示させてる。マッチしなかった場合、インデックスは、-1 になる。
v -g check.v ./check 1 4 bcd
インデックス番号は0が始点、右端の番号は含まれない事に注意だな。
今度は、文字列をスプリットしてみる。スペースでの分解になるな。
fn main() { text := '17 ちはやぶる かみよもきかず たつたがわ からくれないに みずくくるとは' mut re := regex.regex_opt(r' ')! println(re.split(text)) }
他の方法も有るだろうけど(fn (string) split_by_space
)、たまたま目にはいったので試してみた。
sakae@lu:h100$ ./check ['17', 'ちはやぶる', 'かみよもきかず', 'たつたがわ', 'からくれないに', 'みずくくるとは']
map
perlで言う連想配列ね。ああ、連想配列はawkが元祖だったか。
>>> mut cnt := map[string]int >>> cnt['ち'] += 1 >>> cnt['ち'] 1 >>> cnt['た'] 0
後は日本語を、どうやって分解するかだな。探ってみたらruneは Unicode 文字を表す そうで、それを分解するメソッドが用意されてた。
>>> s := 'ちはやぶる' >>> s.runes() [`ち`, `は`, `や`, `ぶ`, `る`] >>> s.runes() [0] ち
pipeに似せてみる
上の句の最初の文字の出現頻度を求めるvlangの例。shellのpipeで簡単に実現出来た けど、日本語混りのプログラミングって事で。
import os fn main() { mut deck := []string{} for s in os.read_lines('genko.txt')! { deck << s.split_by_space()[1] } // cat genko.txt | cut -d' ' -f2 mut cnt := map[rune]int{} for r in deck { k := r.runes()[0] cnt[k] += 1 } // cut -b1-3 and like uniq -c struct Kv { k rune v int } mut res := []Kv{} for a in cnt.keys() { res << Kv{a, cnt[a]} } // collct k,v struct res.sort(a.v > b.v) // sort by values for a in res { println('${a.v}\t${a.k.str()}') } // final output }
パイプ版に添ってコメントを入れておいたので理解できると思う(多少流れは異なるけど)。 vlangにはk,vを 同時に取り出すメソッドが無いっぽい。それから、値でソートする場合は、今回の 様に構造体にしといて、そのエレメントを比較に使ってって指示するのがセオリーだな。
println(res)すると、k: が数値表示されちゃうけど、これがruneの正体ってか文字 コードのint表現だ。文字表現させるには、.str()すれば良い。
[Kv{ k: 12354 v: 17 }, Kv{ k: 12394 v: 8 }, Kv{
実行例。値が同値の場合のキーの順番がパイプ版とは異なっているけど、そんな理由は 知らないぞ。
sakae@lu:h100$ ./cnt 17 あ 8 な 7 わ 6 こ 6 お 6 た : 1 む 1 ほ 1 せ 1 さ 1 め 1 ふ 1 す
game
百人一首の暗記用アプリを作ってみた。下記は、規模を縮小しての実行例。
sakae@lu:h100$ ./game ---------------------------------------------------------------------------- 3 ながながしよを ひとりかもねん 5 こえきくときぞ あきはかなしき 1 わがころもでは つゆにぬれつつ 2 ころもほすちょう あまのかぐやま おくやまに もみじふみわけ なくしかの ? 5 ◎ 5 おくやまに もみじふみわけ なくしかの こえきくときぞ あきはかなしき ---------------------------------------------------------------------------- 1 わがころもでは つゆにぬれつつ 3 ながながしよを ひとりかもねん 2 ころもほすちょう あまのかぐやま 4 ふじのたかねに ゆきはふりつつ たごのうらに うちいでてみれば しろたえの ? 3 4 たごのうらに うちいでてみれば しろたえの ふじのたかねに ゆきはふりつつ ---------------------------------------------------------------------------- 1 わがころもでは つゆにぬれつつ 2 ころもほすちょう あまのかぐやま 3 ながながしよを ひとりかもねん あしびきの やまどりのおの しだりおの ? 1 3 あしびきの やまどりのおの しだりおの ながながしよを ひとりかもねん ---------------------------------------------------------------------------- 1 わがころもでは つゆにぬれつつ 2 ころもほすちょう あまのかぐやま はるすぎて なつきにけらし しろたえの ? 2 ◎ 2 はるすぎて なつきにけらし しろたえの ころもほすちょう あまのかぐやま
そして、そのコード。
import os import rand struct Fuda { id int ue string st string } fn main() { mut deck := []Fuda{} deck = setup_ba()! for i := deck.len; i > 1; i-- { rt := deck.pop() mut dmy := []Fuda{} if deck.len >= 3 { dmy = rand.choose(deck, 3)! } else { dmy = deck.clone() } qa(rt, dmy)! } } fn setup_ba() ![]Fuda { mut rv := []Fuda{} for a in os.read_lines('genko.txt')! { s := a.split_by_space() rv << Fuda{s[0].int(), '${s[1]} ${s[2]} ${s[3]}', '${s[4]} ${s[5]}'} } // rand.shuffle(mut rv)! return rv[..5].clone() } fn qa(rt Fuda, org []Fuda) ! { mut dmy := org.clone() dmy << rt rand.shuffle(mut dmy)! println('----------------------------------------------------------------------------') for x in dmy { println('${x.id:4} ${x.st}') } an := os.input('${rt.ue} ? ') mut gn := '' if rt.id == an.int() { gn = '◎' } println('${gn} ${rt.id} ${rt.ue} ${rt.st}') }
実戦っぽくやるには、 setup_ba()
関数中のコメントを外す。そうすると出題範囲が
ランダムになる。出題件数は、次の行の return rv[..5].clone() で、5を50に変更
する。そうすると、50札分で競技カルタっぽくなる。コメントをそのままにしておいて、rv[40..60]とか、
rv[90..] とかとして、歌番の範囲を指定してもよい。
簡単な解説ってか覚え書き。structで札の構造体の宣言。歌番(id)、上の句(ue)、下の句(st)
を定義。main関数では、 setup_ba()
で、競技の場を作成する。
場に有る札は、配列deckで表わす。
deckから1札をpopして、それをrtに保持する。これを正解札ってか、読み上げ札とする。
次は、下の札の表示用に、ランダムに3札を選ぶ。いわば当て馬用の札になる。
rand.chooseに似たのにrand.sampleが有るけど、こちらは重複選択も認められているので
この場合は、不向きだ。
最後はqa()で、質疑応答をする。これらは、C言語風のfor内にあり、実行回数の制御
を受けている。
setup_ba()
は、原稿ファイルから1行づつreadして、スペースで分解、それを再組立
して、1枚の札にしてる(ちょっと無駄っぽいけど許せ)。注意点は、retrunする時に、
配列のスライスなんで、clone()で複製してる事かな。
qa()は、当て馬のdmyに正解のrtを混ぜてから、シャッフル。続いて、下の句の4択を 表示。面倒なので選択用に歌番も表示してる。本来なら歌番がヒントにならないように、 a,b,c,d等で表示すべきだろう。os.inputでプロンプトとして、正解の上の句を表示し、 選択された番号を入手する。後は、答が正解だった場合に備えて、大変良くできました マーク(◎)を用意した上で、最終的な正解を表示する。
まだ本当に荒削りなプログラムだ。出題される上の句は、time.sleep()とかを利用して ゆっくり表示するとか、空札を読みあげるとかの工夫が出来そうだ。それから、 是非やりたいのは、決り字に色を付ける事。どの解説ページを見ても非常に重要視 されてるからね。
原稿と上記プログラム を用意したんで、我と思わん方は是非改造してみて下さい。
test regex
regexに、const が定義されてて、それを使ってsplit出来るはずなんだけど、自前のコード では、ことごとくエラーになってしまった。
// spaces chars (here only westerns!!) TODO: manage all the spaces from unicode pub const spaces = [` `, `\t`, `\n`, `\r`, `\v`, `\f`]
どう使えばいいの? そんなのtestを調べればヒントが得ら れるでしょ。test == 良質なサンプル と思って覗いてみた。
struct Test_split { src string q string res []string // ['abc','def',...] } const split_test_suite = [ Test_split{'abcd 1234 efgh 1234 ghkl1234 ab34546df', r'\d+', ['abcd ', ' efgh ', ' ghkl', ' ab', 'df']}, Test_split{'abcd 1234 efgh 1234 ghkl1234 ab34546df', r'\a+', ['', ' 1234 ', ' 1234 ', '1234 ', '34546', '']}, : ]
こんなテストの構造体(入力、エンジン、期待出力)を定義。その構造体 を使って、テスト例の配列を作成。
for c, to in split_test_suite { // debug print if debug { println('#${c} [${to.src}] q[${to.q}] (${to.res})') } mut re := regex.regex_opt(to.q) or { eprintln('err: ${err}') assert false continue } re.reset() res := re.split(to.src) if res != to.res { eprintln('err: split !!') if debug { println('#${c} exp: ${to.res} calculated: ${res}') } assert false } }
面白い利用方法だな。c, to って具体的に何を表わす? 上記を取り出し てきて、強制的に表示させたら、
#0 [abcd 1234 efgh 1234 ghkl1234 ab34546df] q[\d+] (['abcd ', ' efgh ', ' ghkl', ' ab', 'df']) #1 [abcd 1234 efgh 1234 ghkl1234 ab34546df] q[\a+] (['', ' 1234 ', ' 1234 ', '1234 ', '34546', ''])
なる程、c はインデックス、to は、その値になるのか。
インタビュー
身近に百人一首をやってた人が居たので、インタビューしてみた。
一人は元同僚の方。失礼ながら、そんな雅な趣味を持ってるとは想像もつかなかった。 受験で古典の点が良くなれば、、と勉強したそうだ。丁度パソコンが出始めた頃で、 CDで発売されてた百人一首ゲームを大枚叩いて購入したそうだ。
もう一人居て、今は2児の母になってる、姪っ子だ。高校に入学した時に、カルタ・クラブに 勧誘されて、はまったそうだ。勉強もしないで、1月かかって、百首をそらんじる様に なったとか。聖地である近江神社まで、試合に行った事があったとか。その時は、 大学生相手の個人戦で勝ち抜き、結構上位まで行ったそうだ。
アホな質問で、相手陣地の札は逆さに見えるはずだけど、そんなんで大丈夫? やってるうちに、パターン認識で苦にならなくなったそうだ。
試合の時は、着物に袴? そんな正装は超疲れるので、正月だけよ。普段はジャージ ですって。色気無いねぇ。優雅とばかり思っていたら、相手の手とぶつかって、 何時も血だらけで、生傷が絶えなかったそうだ。オイラーの想像と大分違うじゃん。