このメモについて

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 用の AIfull: 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 / h7 / 12盤面の幅 / 線までの段数
rows14board に入っている段数 (線をはみ出した2段ぶんを含む)
fps601秒あたりのフレーム数
bottomFirstfalseboard が上の段から並んでいる
colors4色の数。盤面では 1〜4
star6星パネルの番号
clearN3投げた列で、同じ色が縦にこの数ならぶと消える
actionsstring[6]送れる操作の名前
nextLen0次に下りてくる段は見えない
chainMulnumber[16]得点の連鎖倍率 (中身は「得点と攻撃」に)
chainAtknumber[10]攻撃の連鎖倍率
atkDiv14攻撃の重さ (大きいほど弱い)
allClearAtk1.5全消しで上乗せされる攻撃 (段)
clearF45消える演出。この間も操作できる
settleF9消えて上へ詰まってから、連鎖の判定まで
chainWinF26消え終わってから、この間に次を消せば連鎖が続く
pushCdF8自分で段を足したあと、次に足せるまで
garbWaitF30攻撃が確定してから、下り始めるまで
garbRowF14攻撃の段が下りてくる間隔
garbLap12受けている攻撃がこの段数たまるごとに、1回に下りる段が1つ増える
dasF / arrF9 / 2押しっぱなしの横移動の 溜め / 連射 (プレイヤーの設定で変わる)
chars / charobject[] / 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 … 線をはみ出した所。ここまで届いた列は、次の段で負けるおそれがあります (後述)

マスの数字

01〜46
空色星パネル

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)

キー型意味
boardstring(98)盤面
busystring(98)消えている最中のパネルが '1'
pxnumber自分のいる列 (0〜6)
heldobject手に持っているパネル { c: 色, n: 数 }。持っていなければ n が 0
stockobjectHOLD に預けてあるパネル { c, n }
nextarrayいつも空 (次に下りてくる段は見えない)
scorenumber得点
clearednumber消した数の合計
thrownnumber投げた回数
chainnumberいま何連鎖目か。連鎖していなければ 0
aliveboolまだ生きているか
garbagenumber確定して、下りてくるのを待っている段の数
incomingnumber相手の連鎖がまだ続いていて、確定していない段の数
levelnumber段が下りてくる速さの段階 (1〜20)。時間がたつと上がる
canActboolいま送った操作が効くか
curIdnumberthrown と同じ値
phasestring'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一覧 にあります。