おすし将棋のCPUキャラクターは、お辞儀、考え中の顔、有利なときの自信、追い込まれたときの焦りなど、対局に応じて表情を変えます。この記事では、切り替えの条件と優先順位を実装に沿って説明します。
2026年9月11日時点のブラウザ版CPU対局を対象にしています。「勝ち」「負け」「有利」「劣勢」は、特に断りがない限り、CPUから見た状態です。
1. 表情は12種類
| 表情 | 状態キー | 表示する場面 |
|---|---|---|
| お辞儀 | start-bow |
新しい対局を始めた直後 |
| 通常 | normal |
特別な条件がないとき、または評価値がないとき |
| 思考中 | thinking |
AI処理中、または自動思考開始待ち |
| 自信あり | confident |
CPU視点の評価値が +500 以上 |
| 少し劣勢 | slightly-behind |
CPU視点の評価値が −900 より大きく、−350 以下 |
| 大きく劣勢 | badly-behind |
CPU視点の評価値が −900 以下 |
| 好手の反応 | good-move |
CPU着手直後、今回のAI応答の評価値がCPU視点で +500 以上 |
| 悪手を咎められた反応 | blunder |
プレイヤーの応手後、CPU視点の評価が前回より500以上下がり、−350 以下になったとき |
| 王手されている | in-check |
CPUの手番で、CPUの玉が王手されているとき |
| 寝落ち | sleeping |
条件を満たすプレイヤー手番の待機が60秒続いたとき |
| 勝利 | win |
終局し、勝者がCPUと判定されたとき |
| 敗北 | lose |
終局し、勝者がプレイヤーと判定されたとき |
各キャラクターの画像は共通の状態キーに対応しています。以下は10級「たまごのふわり」の例です。

normal
confident
blunder2. 条件が重なったときの優先順位
判定の入口は currentAvatarState() です。対局前、または状態別画像を持たないキャラクターでは、最初に normal を返します。それ以外は次の順番で調べ、最初に当てはまった表情を使います。
- 勝敗:終局し、勝者が分かっていれば
winまたはlose。 - 一時的な反応:有効な
start-bow・good-move・blunderがあれば、その表情。 - 王手:CPUの手番でCPUの玉が王手されていれば
in-check。 - 思考中:AI処理中、または自動思考開始待ちなら
thinking。 - 寝落ち:待機タイマーが寝落ち状態に切り替えていれば
sleeping。 - 評価値:しきい値に応じて
confident・badly-behind・slightly-behind。 - 通常:どれにも当てはまらなければ
normal。
CPUが王手されながら考えている間は「王手」の顔が優先されます。ただし、悪手を咎められた一時反応が有効なら、その間は「悪手」の顔が王手より優先されます。詰みで勝者が決まれば、どちらよりも勝敗の表情が優先されます。
引き分け専用の表情はありません。finished だけでは勝敗の画像にせず、勝者が判定できない場合は下位の条件へ進みます。
3. 評価値をCPUの立場にそろえる
AI応答の score_cp は、その応答に付けた side 側から見た値として扱います。b は先手、w は後手です。まず scoreForBlack() で先手視点にそろえ、次に scoreForSide() でCPU側の視点に変換します。
先手視点の評価 = 応答が先手側ならそのまま、後手側なら符号を反転
CPU視点の評価 = CPUが先手なら先手視点のまま、後手ならもう一度符号を反転
たとえば、評価表示が「先手 +600」でCPUが後手なら、CPU視点では −600 なので「少し劣勢」です。CPUが先手なら +600 なので「自信あり」になります。評価欄は先手視点、表情はCPU視点という違いがあります。
上位の条件がないときの、評価値による切り替えは次のとおりです。境界の値も判定に含まれます。
| CPU視点の評価値 | 表情 |
|---|---|
| +500 以上 | 自信あり |
| −350 より大きく、+500 未満 | 通常 |
| −900 より大きく、−350 以下 | 少し劣勢 |
| −900 以下 | 大きく劣勢 |
| 評価値なし | 通常 |
詰み情報の mate があれば、score_cp より先に使います。変換式は sign(mate) × (30000 − min(abs(mate), 999)) です。たとえば mate = 3 は、その応答側の視点で +29997 として扱われ、その後にCPU視点へ変換されます。詰みの読みがあることと、実際に終局して勝敗が確定することは別です。
評価値はいつ更新される?
表情のために毎フレーム再解析することはありません。評価に応じた表情には、保存してある最新のAI応答 lastResult を使います。
CPUの着手では、その手を決めたAI応答を保存します。プレイヤーの通常の着手では lastResult はいったん null になり、次のCPU思考へ進みます。悪手の比較に必要な前回値は、別の変数 lastAiMoveScore に残しています。
4. 「好手」は +500 以上で出る一時反応
CPUの着手を反映したとき、setAiMoveReaction() が今回のAI応答をCPU視点に変換します。評価値が +500 以上なら good-move を1,700ミリ秒(1.7秒)表示し、それ以外、または評価値がなければ一時反応を解除します。
この判定では、前の手からどれだけ評価が上がったか、最善手との差がどれくらいかは比較していません。有利な状態が続いていれば、CPUの着手ごとに「好手」の顔が出ることがあります。
また、利用するのはその着手を決めたAI応答の評価値です。着手後の盤面を表情用に再解析しているわけではありません。検討画面の棋譜解析による好手・悪手の分類とは、別の判定です。
通常は1.7秒後に評価値や局面に応じた表情へ戻りますが、その前にプレイヤーが指した場合は一時反応を解除します。終局して勝者が決まった場合は、勝敗の表情が優先されます。
5. 「悪手」は相手に咎められてから出る
blunder はCPUが悪い手を選んだ瞬間に出すものではありません。CPUの前回の着手時に保存した評価値と、プレイヤーが応じたあとの次のAI応答の評価値を比較します。両方ともCPU視点です。
shouldShowPunishedBlunder() は、次の2条件を両方満たすと真になります。
今回の評価値 <= -350
前回の評価値 - 今回の評価値 >= 500
前回値または今回値がなければ、この反応は出しません。
| 前回 → 今回 | 悪化幅 | 悪手の反応 | 理由 |
|---|---|---|---|
| +200 → −400 | 600 | 出る | 500以上悪化し、今回が −350 以下 |
| +150 → −350 | 500 | 出る | 2つの境界条件をちょうど満たす |
| +1000 → +300 | 700 | 出ない | 今回の評価が −350 以下ではない |
| −100 → −550 | 450 | 出ない | 悪化幅が500に届かない |
| 評価値なし → −900 | 比較不可 | 出ない | 前回値がない |
表情を見せてからCPUが指す
通常の進行は次の順です。
- CPUが指し、そのAI応答の評価値を
lastAiMoveScoreに保存する。 - プレイヤーが応手を指す。
- 次のCPU手番でAIに問い合わせ、返ってきた評価値で悪手の条件を判定する。
- 合法な指し手が確認でき、条件を満たす場合は
blunderを表示する。 - 1,200ミリ秒(1.2秒)待ってから、CPUの次の着手を反映する。
一時反応そのもののタイマーは1.7秒ですが、この経路では約1.2秒後のCPU着手で一時反応を再判定し、解除します。したがって、通常の「悪手」の見せ場は約1.2秒です。ブラウザの処理状況によって実時間は前後します。
AI応答が投了や宣言勝ちなら、この比較より先に終局処理へ進みます。また、AIのポカ率などの設定値を直接見て表情を選んでいるわけではありません。
6. お辞儀・思考中・寝落ちの時間管理
お辞儀は開始時に1.3秒
新しい対局では showAvatarMoment('start-bow', 1300) を呼びます。先後を表示する開始演出は別の処理なので、お辞儀の1.3秒は開始演出全体の長さを意味しません。
思考中には開始待ちも含む
isAiThinking() は busy が真、または autoAiTimer が存在すると真になります。通常の自動応手は250ミリ秒待ってAIへの問い合わせを始めるため、その開始待ちも思考中に含まれます。「次の一手」ヒント取得の hintBusy は、この判定には含まれていません。
寝落ちはプレイヤーの手番で60秒待ったとき
shouldStartSleepTimer() は、次の条件をすべて満たすと寝落ちタイマーを開始できます。
- 対局が始まっていて、まだ終局していない。
- 選択中のキャラクターに
sleepingの画像がある。 - お辞儀・好手・悪手の一時反応がない。
- AI思考中でも、自動思考開始待ちでもない。
- デバッグ用の「両操作」ではなく、プレイヤーの手番である。
この状態で60秒が経過し、タイマー発火時にも条件が成立していれば opponentSleeping を真にします。一時反応がある間は待ち始めないため、「好手」の顔が1.7秒続いた場合は、基本的にその終了後から60秒を数えます。
手を指すと寝落ち状態と待機タイマーを解除します。単なるマウス移動や画面操作の無操作時間を測っているわけではありません。条件が成立している間の再描画だけでは、タイマーは最初から数え直しません。
7. 画像の切り替えと表示の工夫
キャラクターごとの CPU_CHARACTERS 定義には、基本画像の image と、状態別画像の images があります。状態の判定と、画像を画面へ出す処理は分けています。
通常の画像選択は character.images[state]、次に character.image の順です。どちらもなければ画像用の指定を外し、CSSで描く基本アバターになります。未定義の状態キーに対して、自動的に images.normal を探す処理ではありません。また、画像ファイルの読み込みに失敗した場合の別画像への再試行は、この選択処理にはありません。
思考中などに avatarLayers[state] が定義されている場合は、1枚の画像ではなく、本体の body と吹き出しなどの overlays を重ねて描画します。レイヤー定義の body がなければ、通常の画像選択へ進みます。
renderOpponentAvatar() は、対戦相手欄と盤の近くのキャラクターに同じ状態を渡すため、両方の表情がそろいます。画像のURL・状態・レイヤー構成が変わっていなければ、描画キーで判定して不要な差し替えを省きます。
対局するキャラクターを選んだときには、基本画像・状態別画像・レイヤー画像を先読みします。レイヤー表示から1枚画像へ戻る際、次の画像の準備が終わっていなければ、準備完了までレイヤーを保持します。待っている間に表示要求が変わった場合は、古い要求を反映しません。
寝落ちの画像には、2.8秒周期でわずかに上下・回転・拡縮する呼吸アニメーションを付けています。端末側で動きを減らす設定(prefers-reduced-motion: reduce)が有効なら、このアニメーションを止めます。
8. 状態のリセットと勝敗判定
表情の主な保持状態は、一時反応の avatarMoment、その終了タイマー、寝落ちタイマー、寝落ちフラグ opponentSleeping、悪手比較用の lastAiMoveScore です。
新規対局・キャラクター変更・待ったなどの局面切り替えでは、関係するタイマーや前回評価をリセットし、前の局面の反応を引き継がないようにしています。プレイヤーの着手でも一時反応と寝落ちを解除しますが、悪手比較用の前回評価は次のAI応答まで保持します。
winnerSideForFinished() は、詰み、投了、時間切れ、宣言勝ちを確認します。詰みでは手番側の反対を勝者とし、投了・時間切れでは該当側の反対、宣言勝ちでは宣言側を勝者とします。その勝者がCPUかどうかで win と lose を選びます。
9. 調整に使う定数
以下は shogi-cpu.js に定義されている現在の値です。時間はミリ秒、評価値と悪化幅はCPU視点に変換した値で判定します。
| 定数 | 値 | 役割 |
|---|---|---|
CPU_GREETING_DURATION_MS |
1300 | 開始時のお辞儀 |
CPU_SLEEP_DELAY_MS |
60000 | 寝落ちまでの待機 |
CPU_MOVE_REACTION_DURATION_MS |
1700 | 好手・悪手の一時反応タイマー |
CPU_BLUNDER_REACTION_HOLD_MS |
1200 | 悪手の表情を出してから次のCPU着手までの待機 |
CPU_CONFIDENT_SCORE |
500 | 自信あり・好手の基準 |
CPU_BEHIND_SCORE |
-350 | 少し劣勢・悪手反応の到達条件 |
CPU_BADLY_BEHIND_SCORE |
-900 | 大きく劣勢の基準 |
CPU_PUNISHED_SCORE_DROP |
500 | 悪手反応に必要な悪化幅 |
10. 実装の参照先
記事の判定条件は、次のファイルと関数をもとに整理しています。
| ファイル | 主な参照箇所 |
|---|---|
public/game/shogi/js/shogi-cpu.js |
CPU_CHARACTERS、currentAvatarState()、scoreForBlack()、scoreForSide() |
| 同上 | commitMove()、requestAiMove()、setAiMoveReaction()、shouldShowPunishedBlunder() |
| 同上 | showAvatarMoment()、syncSleepTimer()、shouldStartSleepTimer()、winnerSideForFinished() |
| 同上 | applyOpponentAvatar()、renderAvatarLayers()、preloadCharacterImages()、queueAvatarImageRender() |
public/game/shogi/css/shogi.css |
状態別のアバター表示、寝落ちの呼吸アニメーション、動きを減らす設定 |
docs/shogi-cpu-characters.md |
キャラクター定義・表情画像の制作方法 |
実際の対局では、評価値だけでなく、指した直後の反応や王手、待っている時間にも注目すると、表情の切り替わりが分かります。CPU対局で試す。