0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

しりとりの「最後の文字」を、伸ばし棒と小さい字ごと1つの関数で決める

0
Posted at

背景

ポケモンの名前だけでしりとりをするブラウザゲームを置いています。
コードにしようとすると「ピカチュウの次は何?」で手が止まりました…。
判定部分を実際のコードから読み直した備忘録です。

最後の文字を決める2つの関数

判定の中心は headChar(最初の文字)と tailChar(最後の文字)の2つだけだ。
どちらも、画面に出す名前ではなく「読み」を通してから1文字を取り出す。

下記がゲーム本体の定義です(SMALL だけ1行に詰めました)。

const YOMI = {
  "ニドラン♀": "ニドランメス",
  "ニドラン♂": "ニドランオス",
  ポリゴン2: "ポリゴンツー",
  ポリゴンZ: "ポリゴンゼット",
};
const SMALL = { ャ: "ヤ", ュ: "ユ", ョ: "ヨ", ッ: "ツ", ァ: "ア", ィ: "イ", ゥ: "ウ", ェ: "エ", ォ: "オ" };
const norm = (c) => SMALL[c] || c;
const reading = (n) => (YOMI[n] || n).replace(/[・:]/g, "");
const headChar = (n) => norm(reading(n)[0]);
function tailChar(n) {
  const s = reading(n);
  let i = s.length - 1;
  while (i > 0 && s[i] === "ー") i--;
  return norm(s[i]);
}

reading は、記号や数字を含む4匹だけを YOMI で読みに置き換え、「・」「:」を消す。
tailChar は末尾から「ー」を読み飛ばし、止まった文字を norm で大きい字に直す。

なぜ名前を直接いじらないかというと、画面の表示は元の名前のまま出したいからです。
読みの変換を関数の中に閉じておけば、表示用の文字列を壊さずに済む。

実際に動かして確かめる

ゲームの HTML から、const NAMES = [ から const ROW_COLORS の手前までを切り出して Node.js で評価しました。
この範囲に NAMES と上の関数が入っているので DOM 無しで動きます(index.html は公開中の HTML のコピー)。

const fs = require("fs");
const html = fs.readFileSync("index.html", "utf8");
const src = html.slice(html.indexOf("const NAMES = ["), html.indexOf("const ROW_COLORS"));
const { NAMES, headChar, tailChar } = new Function(src + "; return { NAMES, headChar, tailChar };")();

console.log("NAMES:", NAMES.length);
for (const n of ["ピカチュウ", "ミュウツー", "カイリュー", "リザードン", "ポリゴン2"])
  console.log(n, "→", tailChar(n));
const uu = NAMES.filter((n) => headChar(n) === "ウ");
console.log("「ウ」ではじまるポケモン " + uu.length + "匹");
console.log("「ン」で終わる:", NAMES.filter((n) => tailChar(n) === "ン").length);

出力は下記でした。

NAMES: 1025
ピカチュウ → ウ
ミュウツー → ツ
カイリュー → ユ
リザードン → ン
ポリゴン2 → ツ
「ウ」ではじまるポケモン 22匹
「ン」で終わる: 143

カイリューは「ー」を飛ばして「ュ」、それを大きくして「ユ」になっている。
ポリゴン2は読み「ポリゴンツー」経由で「ツ」。
1025匹中143匹が「ン」終わりで、1割強が地雷です。

候補の数と「ン」の判定

画面の「〇〇ではじまるポケモン △匹」は、まだ使っていない名前の集合 pool(Set)から headChar が一致するものを集めた数だ。

function candidates(ch) {
  const out = [];
  for (const n of pool) if (headChar(n) === ch) out.push(n);
  return out.sort((a, b) => a.length - b.length || a.localeCompare(b, "ja"));
}

ゲーム開始直後は pool が全1025匹なので、「ウ」なら上の実行結果と同じ 22匹 と出る。
選ばれた名前は pool.delete(name) で消えるので、同じポケモンは2回出せない。
tailChar(n) === "ン" の候補は縞模様のボタンにし、選んだ瞬間も同じ判定で負けにしている。

CPU の選び方

CPU は、まず「ン」で終わらない候補に絞る(無ければ仕方なく全候補)。
その先頭60件について、「その名前を出したら相手の候補が何匹残るか」を数え、一番少ない手を選ぶ。

for (const n of all.slice(0, 60)) {
  const s = candidates(tailChar(n)).length + (tailChar(n) === "ン" ? 1000 : 0);
  if (s < bestScore) { bestScore = s; best = n; }
}
const pick = Math.random() < 0.34 ? all[Math.floor(Math.random() * all.length)] : best;

all は上で絞った候補の配列で、candidates と同じく短い名前順に並んでいる。
同じ切り出しで開始直後の「ウ」を試すと、「ン」以外の19匹から ウールー(尻は「ル」、続く候補10匹)が最善手になりました。
ただし 34% の確率でわざと無作為に選ぶ。
毎回最善手だと勝ちにくすぎるので、ここで強さを調整している様子です。

ゲームはこちらで遊べます。
https://hashito.biz/tools/pokeshiritori/

しりとりを実装するなら、名前を直接比べずに「読みから頭と尻の1文字を返す関数」を先に作ると、候補の数も負け判定も CPU もその2関数の組み合わせで書けます。


本記事はAI補助で執筆した、個人開発の紹介記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?