1マスを1バイトで表す
cell[y][x] には、次の4状態のどれか1つが入ります。複数の bool 配列を重ねず、盤面1枚だけで判定できます。
bool[16][16] を持つ場合より512バイト節約し、RAM 2KBのCH32V003に探索用の余裕を残します。Xは右へ、Yは下へ
左上が (0,0)。添字はX・Yの順ではなく cell[Y][X] です。
進入できる条件
盤外でなく、そのマスが EMPTY なら進めます。岩も両者の体も同じ判定で除外できます。
bool passable(int16_t x, int16_t y) {
if (x < 0 || x >= 16 ||
y < 0 || y >= 16) return false;
return cell[y][x] == EMPTY;
}空にしてから、岩を1個ずつ記録
CMD_RESET で全マスを EMPTY にし、続く岩座標を ROCK に変えます。岩はWeb側で180°回転対称に配置されます。
受信のたびに1マス更新
case CMD_ROCK:
cell[buf[2]][buf[1]] = ROCK;自分と相手、2つの頭を置く
Webは「自分の頭」「相手の頭」「自分の初期方向」を送ります。2台対戦では、各デバイスへ自分視点に並べ替えて渡します。
initRound()
myX=3; myY=8; oppX=12; oppY=7; cell[8][3] = MINE; cell[7][12] = OPP; curDx=1; curDy=0;
進めるマスと「取り合いになるマス」を分ける
壁・岩・両者の体を除いた候補を作ります。さらに、相手の頭から1マスの場所は、相手も次に入れるため正面衝突の危険があります。
相手の「現在地」だけでは足りない
両者が同時に同じ空きマスへ入ると引き分けです。候補と相手の頭のマンハッタン距離が1なら、評価を40点下げます。
dxo = abs(nx - oppX); dyo = abs(ny - oppY); if (dxo + dyo == 1) score -= 40;
BFSで、自分が生き残れる広さを数える
各候補から到達できる EMPTY の数をBFSで数え、取り合いペナルティを引いた点が最大の方向を選びます。
評価例
score = countReachable(nx, ny);
if (相手も入れる) score -= 40;
// 最大スコアの dx,dy を採用配布スケッチは「自分の空間」だけを評価するため、CPU Lv3と互角ですがLv4のボロノイで壁に当たります。
決めた「方向」だけをWebへ返す
座標は自分で確定しません。vs.sendDir(dx,dy) で次の向きを送り、Web側の同時移動を待ちます。
solveTick()
int8_t dx=curDx, dy=curDy; computeNextDir(dx, dy); curDx=dx; curDy=dy; vs.sendDir(dx, dy);
CMD_DIR を待ってから次のティックへ進むため、USB応答速度で勝敗が決まりません。Webが2匹を同時に1マス進める
両者の予定地を先に計算し、移動前の同じ盤面を使って死亡判定します。生存した場合だけ、両者を一緒に盤面へ書き込みます。
対戦特有の衝突
if (n1.x == n2.x && n1.y == n2.y)
dead1 = dead2 = true;
// 生存時だけ両者を記録移動後の両者の頭から、体を復元する
蛇は縮まないため、「体」は頭が通った全マスです。Webは毎回、両者の新しい頭だけを送れば十分です。
受け取って、記録して、次を考える
myX=buf[1]; myY=buf[2]; oppX=buf[3]; oppY=buf[4]; cell[myY][myX] = MINE; cell[oppY][oppX] = OPP; solveTick();
「自分が広い」から「相手より広い」へ
CPU Lv4は、各空きマスへどちらが先に着けるかを2回のBFSで比べます。緑の陣地数−赤の陣地数を最大化すると、相手を狭い側へ追い込めます。
配布スケッチを超える考え方
myDist = BFS(myX, myY); oppDist = BFS(oppX, oppY); if (myDist < oppDist) mine++; if (oppDist < myDist) theirs++; score = mine - theirs;
CPU Lv5はさらに「自分の手→相手の最善応手」まで仮に打つ、深さ2のミニマックスを使います。
配布スケッチを超える、勝つためのヒント
最大空間優先は「自分がすぐ詰まない」ための守りです。勝つには、同じ1手で相手の未来がどう狭くなるかまで評価します。
BFSの到達可能数だけでは、両者が使える共通空間もすべて「自分の空間」として数えます。狭い通路や分岐点を先に取ると、自分の壁で相手の移動範囲を切り離せます。
各候補を仮に打ち、自分の頭と相手の頭からBFS距離を作ります。自分が先に着くマスを増やし、相手が先に着くマスを減らす手を選びます。
自分に都合のよい相手の手ではなく、相手が返せる手のうち、自分の評価を最も下げる手を採用して比較します。これが深さ2のミニマックスです。
各「自分の手」について {
worst = 相手の全合法手の 最小評価値;
}
答え = worst が最大になる自分の手;高度な評価関数でも、正面衝突、同点時の固定方向、仮想手の戻し忘れがあると安定して勝てません。
SnakeVS.ino 全体を一言ずつ
cell[16][16]EMPTY・MINE・OPP・ROCKを1枚で管理。initRound()両者の頭と自分の向きを初期化。passable()壁・岩・両者の体をまとめて除外。countReachable()候補から生き残れる空間をBFSで数える。computeNextDir()取り合いを減点し、最大空間の方向を選ぶ。CMD_TICK両者の頭を軌跡として記録し、次の方向を返す。