このメモについて
trisblocks の AI DEVELOP で、MAGI BLOCKS の AI を書くための取り決めを全部並べたものです。
「まず動かすところまで」は ① 最初の作り方 にあります。こちらは手元に置いて引く用なので、上から順に読む必要はありません。
AI に渡しているのは、人が画面を見て分かることだけです。残りフレームのような中の値、次に下りてくる段の中身、攻撃の端数、乱数は入っていません。
つなぎ方
AI DEVELOP で MAGI BLOCKS を選び、ファイルをえらぶ で自分の HTML を指定します。ゲームの中の隔離された枠 (sandbox="allow-scripts") で動き、BroadcastChannel('tb-ai') はゲーム側が橋渡しします。アップロードは要りません。
HTML をブラウザで直接開いてもつながりません。かならず「ファイルをえらぶ」から指定してください。
やり取りの流れ
ゲームから出るものはすべて from:'game' を持ちます。AI から出すものは from:'ai' にしてください。
| 向き | 中身 | いつ |
|---|---|---|
| AI → ゲーム | { from:'ai', type:'hello', name, game:'magi' } | 起動した時に1回。名乗り |
| ゲーム → AI | { type:'hello', meta } | 名乗りと pong への返事。盤面の決まりごとを渡す |
| ゲーム → AI | { type:'ping' } | AI DEVELOP の画面で、AI の返事を待っている間 (350ms ごと) |
| AI → ゲーム | { from:'ai', type:'pong', name, game:'magi' } | 呼びかけへの返事。これで「決定」が押せるようになる |
| ゲーム → AI | { type:'state', game, f, applied, me, opp } | 対戦中、毎フレーム (60/秒)。1回およそ 930 バイト |
| AI → ゲーム | { from:'ai', act:[ ... ] } | いつ送ってもよい。次のフレームの頭で反映される |
| ゲーム → AI | { type:'bye' } | タイトルへ戻った時 |
MAGI BLOCKS 用だと名乗る (game)
hello と pong の両方に game: 'magi' を添えます。どちらかが抜けると、その時点で TRISBLOCKS 用に戻ったものとして扱われ、 「読み込み失敗: この AI は TRISBLOCKS 用です」 と出て操作を受け付けません。
ゲーム側からも meta.game と state.game で 'magi' が届くので、AI の側で確かめることもできます。
state の外枠
{
type: 'state', from: 'game',
game: 'magi',
f: 1234, // フレーム番号
applied: 87, // ゲームがこれまでに受け付けた操作の数
mode: 'cpu',
me: { ... }, // 自分
opp: { ... } // 相手
}
applied は、ゲームが受け取って盤面へ入れた操作の通算数です。送った操作が届いたかどうかは、 盤面の見た目ではなくこの数で判ります。4個送ったあと applied が 4 増えていれば、その 4個は届いています。
- カウントダウン中やポーズ中に届いた操作は捨てられ、
appliedも増えません - 増えるのは「受け付けた」数です。違う色を持ったまま
grabを送った時のように、受け付けたうえで何も起きないこともあります
相手の盤面 (opp)
opp は me と同じ形です。opp.full を必ず見てください。
| 相手 | opp の中身 |
|---|---|
| CPU、MAGI BLOCKS 用の AI | full: true。me と同じものが全部入る |
| ほかのゲームの AI (「AI と対戦」で選べる) | full: false。何も無い盤面が入る。読めるのは、自分と同じゲームの盤面だけです |
入力の書き方
書き方は3通りあり、混ぜてもかまいません。MAGI BLOCKS の操作は、どれも1回ぶんの書き方で足ります。
| 書き方 | 意味 |
|---|---|
['grab'] / 'grab' | 押してすぐ離す = 1回ぶん |
['left', 1] | 押したままにする |
['left', 0] | 離す |
{a:'grab', d:1} | 上と同じ。d を省くと1回ぶん |
// 1フレームぶんをまとめて送ってよい。 この順に反映される
CH.postMessage({ from:'ai', act: [ ['left'], ['left'], ['grab'], ['right'], ['throw'] ] });
- 横の1回押し (
['left']['right']) は、その場で1列動きます - 1フレームで反映するのは 64個までです。それを大きく超えて溜まると、追いつけないと見なして列ごと捨てます
- 知らない名前の操作は、黙って無視されます
操作の名前
| 名前 | 動き |
|---|---|
left / right | 横へ1列。端では動かない |
grab | いまの列の、いちばん下のパネルを取る。同じ色が縦に続いていれば、まとめて取る |
throw | 持っているパネルを全部、いまの列のいちばん下へ投げる |
hold | 持っているパネルを預ける / 預けてあるものと入れかえる |
push | 自分で段を1つ足す |
効かない時
| 操作 | 効かない時 |
|---|---|
grab | 列が空 / いちばん下が消えている最中 / 違う色を持っている (同じ色なら取り足せる) |
throw | 何も持っていない |
hold | 手にも HOLD にも何も無い |
push | 消えている最中 / 連鎖が続いている間 / 前に足してから pushCdF たっていない |
持てるのは1色だけです。別の色を取りたい時は、いまのものを投げるか、hold で預けます。
盤面の決まりごと (meta)
hello に乗ってきます。時間の単位はすべてフレーム (1/60秒) です。
| キー | 値 | 意味 |
|---|---|---|
game | 'magi' | ゲームの種別 |
w / h | 7 / 12 | 盤面の幅 / 線までの段数 |
rows | 14 | board に入っている段数 (線をはみ出した2段ぶんを含む) |
fps | 60 | 1秒あたりのフレーム数 |
bottomFirst | false | board が上の段から並んでいる |
colors | 4 | 色の数。盤面では 1〜4 |
star | 6 | 星パネルの番号 |
clearN | 3 | 投げた列で、同じ色が縦にこの数ならぶと消える |
actions | string[6] | 送れる操作の名前 |
nextLen | 0 | 次に下りてくる段は見えない |
chainMul | number[16] | 得点の連鎖倍率 (中身は「得点と攻撃」に) |
chainAtk | number[10] | 攻撃の連鎖倍率 |
atkDiv | 14 | 攻撃の重さ (大きいほど弱い) |
allClearAtk | 1.5 | 全消しで上乗せされる攻撃 (段) |
clearF | 45 | 消える演出。この間も操作できる |
settleF | 9 | 消えて上へ詰まってから、連鎖の判定まで |
chainWinF | 26 | 消え終わってから、この間に次を消せば連鎖が続く |
pushCdF | 8 | 自分で段を足したあと、次に足せるまで |
garbWaitF | 30 | 攻撃が確定してから、下り始めるまで |
garbRowF | 14 | 攻撃の段が下りてくる間隔 |
garbLap | 12 | 受けている攻撃がこの段数たまるごとに、1回に下りる段が1つ増える |
dasF / arrF | 9 / 2 | 押しっぱなしの横移動の 溜め / 連射 (プレイヤーの設定で変わる) |
chars / char | object[] / string | 選べるキャラクター [{id,name}] / いま選ばれている id |
盤面の読み方
board は 14段 × 7列 = 98文字です。上の段から、左から右へ並びます。
index = y * 7 + x // 0 ≦ x < 7, 0 ≦ y < 14。 y = 0 がいちばん上
cell = +board[index]
パネルは上からぶら下がっているので、どの列も必ず上から詰まっています。空きは下にしかありません。 新しい段は上 (y = 0) に入り、全体が1段ずつ下へ押されます。
// 試合が始まった時の board (3段ぶん入っている)
"2242322" // y = 0
"2433421" // y = 1
"4423243" // y = 2 ← 各列のいちばん下。 取れるのはここ
"0000000" // y = 3 から下は空
…
y = 0〜11… 線の内側の12段y = 12〜13… 線をはみ出した所。ここまで届いた列は、次の段で負けるおそれがあります (後述)
マスの数字
| 0 | 1〜4 | 6 |
|---|---|---|
| 空 | 色 | 星パネル |
busy は board と同じ並びの文字列で、消えている最中のパネルの所が '1' になります。 消えている最中のパネルは取れませんし、つながりにも数えません。
列の高さと、いちばん下のパネル
function colH(me, x) { // その列に入っているパネルの数
let y = 0;
while (y < meta.rows && me.board[y * meta.w + x] !== '0') y++;
return y;
}
const h = colH(me, x);
const bottom = h ? +me.board[(h - 1) * meta.w + x] : 0; // いちばん下の色。 空の列は 0
state (me / opp)
| キー | 型 | 意味 |
|---|---|---|
board | string(98) | 盤面 |
busy | string(98) | 消えている最中のパネルが '1' |
px | number | 自分のいる列 (0〜6) |
held | object | 手に持っているパネル { c: 色, n: 数 }。持っていなければ n が 0 |
stock | object | HOLD に預けてあるパネル { c, n } |
next | array | いつも空 (次に下りてくる段は見えない) |
score | number | 得点 |
cleared | number | 消した数の合計 |
thrown | number | 投げた回数 |
chain | number | いま何連鎖目か。連鎖していなければ 0 |
alive | bool | まだ生きているか |
garbage | number | 確定して、下りてくるのを待っている段の数 |
incoming | number | 相手の連鎖がまだ続いていて、確定していない段の数 |
level | number | 段が下りてくる速さの段階 (1〜20)。時間がたつと上がる |
canAct | bool | いま送った操作が効くか |
curId | number | thrown と同じ値 |
phase | string | 'play' / 'wait' / 'dead' |
MAGI BLOCKS には state.match がありません。
消え方と連鎖
- 投げた列のいちばん下で、同じ色が縦に
clearN(3) 個以上並ぶと、そこから上下左右につながった同じ色がまとめて消えます - 星パネルを縦に3つ並べると、星と一緒に、星のすぐ上にあるパネルと同じ色が盤面からすべて消えます
- 消えたあと、その下にあったパネルは上へ詰まります。詰まって動いたパネルを含む縦3つ以上ができると、それも消えて連鎖になります
- 消えている間 (
clearF) も操作できます。消えている最中から、消え終わってchainWinFたつまでの間に自分で投げて消しても連鎖が1つ進みます - 消えている間と連鎖が続いている間は、新しい段も、相手からの攻撃も下りてきません
得点と攻撃
得点 = 10 × 消した数 × chainMul[連鎖 - 1]
攻撃 (段) = (消した数 - 2) × chainAtk[連鎖 - 1] ÷ atkDiv
chainMul = [1, 2, 4, 6, 9, 12, 16, 20, 25, 30, 36, 42, 49, 56, 64, 72]
chainAtk = [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
- 攻撃の端数は切り捨てず、次へ持ち越します。3個を1連鎖目で消すと 1 × 1 ÷ 14 = 0.07段、5個を3連鎖目で消すと 3 × 6 ÷ 14 = 1.29段です
- 連鎖が表の長さを超えたら、最後の値がそのまま使われます
- 盤面が空で、手にも HOLD にも何も無い状態で連鎖が終わると全消しです。次の連鎖の1回目に
allClearAtk段が上乗せされます - 対戦が長引くと、攻撃に倍率が掛かります
自分の攻撃は、まず自分が受けているぶんを打ち消します。余ったぶんだけが相手へ飛びます。
攻撃を受けるまで
- 相手の連鎖が続いている間は
incoming、連鎖が終わって確定するとgarbageに移ります - 確定から
garbWaitFたつと、garbRowFごとに下りてきます - ためこむほど、一度にたくさん下りてきます。 1回に下りる段は次の式で決まります
1回に下りる段 = 1 + floor((garbage + incoming) ÷ garbLap)
// 12段に満たなければ1段ずつ / 12段たまると2段ずつ / 24段で3段ずつ …
受けているぶんが少ないうちに消して打ち消すほうが、ずっと楽です。
段が下りてくる決まりと、負けの条件
- 何もしなくても、一定の間隔で上から1段ずつ下りてきます。
levelが上がるほど間隔が短くなります - 盤面がほとんど空になると、次の段はすぐに下りてきます
- 段が下りてきた直後に、パネルが 14個以上になった列があると負けです (線を2段はみ出す)。13個の列は、次の段が来るまで負けません
- 消えている最中のパネルがある列は、数えません。投げただけでは負けません
キャラクターと名前
盤面の背景になるキャラクターと、盤面の下に出る名前は、AI 側から指定できます。どちらもどのメッセージに添えてもかまいません。
| やり方 | いつ効くか |
|---|---|
{ from:'ai', type:'pong', name:'わたしのAI', game:'magi' } | 名前。16文字まで |
{ from:'ai', type:'pong', char:'c3', game:'magi' } | キャラクター。次に始める試合から |
{ from:'ai', type:'char', id:'c3' } | いつ送ってもよい。試合中に送ると次の試合から |
選べる id は meta.chars に一覧で入っています。知らない id は黙って無視されます。名乗らなかった場合は「あなたの AI」と出ます。
対戦相手として投稿する
自分の AI を、ほかの人が選べる対戦相手として上げることができます。AI DEVELOP の アップロード準備 から進みます。
関門は、MAGI BLOCKS 用の AI EyelidsMoon に10連勝することです。
- 「ファイルをえらぶ」で選んだ AI が動いていること、ファイルが 500KB 以下であることが条件です
- 10戦ともふつうの対戦で、1敗したらはじめからやり直しです
- 抜けると名前 (16文字まで) と作った人 (12文字まで) を入れる画面になり、アップロードする を押すと「AI と対戦」の一覧に並びます
- 同じ名前の AI がすでにある時は、上書きするかどうかを聞かれます。上書きしても、レートと成績は引き継がれます
- 上げたあとは、AI DEVELOP の 投稿済み AI の 調整する から、名前や備考を直したり消したりできます
上げた AI は、同じファイルのまま相手役としても動きます。その時は me が自分 (相手役)、opp がプレイヤーの盤面です (プレイヤーがほかのゲームで遊んでいる時は、opp は何も無い盤面になります)。 相手役の時は ping が来ません。起動した時の hello に game: 'magi' を添えておけば、それで種別が伝わります。
つまずきやすい所
- 返事は待ちません。 入力は届いた次のフレームの頭で反映されるので、実際には 1〜2フレーム遅れて入ります。そのぶん AI が重くても画面は止まりません
y = 0がいちばん上です。 PAYA BLOCKS とは上下が逆で、取れるパネルは各列のいちばん大きいyにあります- 持てるのは1色だけです。 違う色を持ったままの
grabは効かないので、held.cを見てから取りに行ってください - 消している間は、段が下りてきません。 危ない時ほど、何かを消し続けるのが守りになります
- 攻撃はためこまないでください。
garbage + incomingがgarbLapを超えると、一度に2段、3段と下りてきます nextは空です。 次に下りてくる段の中身は、人にも見えていませんhelloとpongの両方にgame: 'magi'を。 片方だけだと「読み込み失敗」になります
ほかのゲームの API は、PAYA BLOCKS ② API一覧 と trisblocks ② API一覧 にあります。