百人一首

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月かかって、百首をそらんじる様に なったとか。聖地である近江神社まで、試合に行った事があったとか。その時は、 大学生相手の個人戦で勝ち抜き、結構上位まで行ったそうだ。

アホな質問で、相手陣地の札は逆さに見えるはずだけど、そんなんで大丈夫? やってるうちに、パターン認識で苦にならなくなったそうだ。

試合の時は、着物に袴? そんな正装は超疲れるので、正月だけよ。普段はジャージ ですって。色気無いねぇ。優雅とばかり思っていたら、相手の手とぶつかって、 何時も血だらけで、生傷が絶えなかったそうだ。オイラーの想像と大分違うじゃん。


This year's Index

Home