このメモについて

trisblocks の AI DEVELOP で、PAYA BLOCKS の AI を書くための取り決めを全部並べたものです。

「まず動かすところまで」は ① 最初の作り方 にあります。こちらは手元に置いて引く用なので、上から順に読む必要はありません。

AI に渡しているのは、人が画面を見て分かることだけです。残りフレームのような中の値、NEXT より先の組、得点の端数、乱数は入っていません。


つなぎ方

AI DEVELOP で PAYA BLOCKS を選び、ファイルをえらぶ で自分の HTML を指定します。ゲームの中の隔離された枠 (sandbox="allow-scripts") で動き、BroadcastChannel('tb-ai') はゲーム側が橋渡しします。アップロードは要りません。

HTML をブラウザで直接開いてもつながりません。かならず「ファイルをえらぶ」から指定してください。


やり取りの流れ

ゲームから出るものはすべて from:'game' を持ちます。AI から出すものは from:'ai' にしてください。

向き中身いつ
AI → ゲーム{ from:'ai', type:'hello', name, game:'paya' }起動した時に1回。名乗り
ゲーム → AI{ type:'hello', meta }名乗りと pong への返事。盤面の決まりごとを渡す
ゲーム → AI{ type:'ping' }AI DEVELOP の画面で、AI の返事を待っている間 (350ms ごと)
AI → ゲーム{ from:'ai', type:'pong', name, game:'paya' }呼びかけへの返事。これで「決定」が押せるようになる
ゲーム → AI{ type:'state', game, f, applied, me, opp, match }対戦中、毎フレーム (60/秒)。1回およそ 1250 バイト
AI → ゲーム{ from:'ai', act:[ ... ] }いつ送ってもよい。次のフレームの頭で反映される
ゲーム → AI{ type:'bye' }タイトルへ戻った時

PAYA BLOCKS 用だと名乗る (game)

hello と pong の両方に game: 'paya' を添えます。どちらかが抜けると、その時点で TRISBLOCKS 用に戻ったものとして扱われ、 「読み込み失敗: この AI は TRISBLOCKS 用です」 と出て操作を受け付けません。

ゲーム側からも meta.game と state.game で 'paya' が届くので、AI の側で確かめることもできます。


state の外枠

    {
      type: 'state', from: 'game',
      game: 'paya',
      f: 1234,              // フレーム番号
      applied: 87,          // ゲームがこれまでに受け付けた操作の数
      mode: 'cpu',
      me:  { ... },         // 自分
      opp: { ... },         // 相手
      match: { ... }        // 試合全体のこと
    }

applied は、ゲームが受け取って盤面へ入れた操作の通算数です。送った操作が届いたかどうかは、 盤面の見た目ではなくこの数で判ります。4個送ったあと applied が 4 増えていれば、その 4個は届いています。

  • カウントダウン中やポーズ中に届いた操作は捨てられ、applied も増えません
  • 増えるのは「受け付けた」数です。壁に向かって left を送った時のように、受け付けたうえで何も起きないこともあります

相手の盤面 (opp)

opp は me と同じ形です。opp.full を必ず見てください。

相手opp の中身
CPU、PAYA BLOCKS 用の AIfull: true。me と同じものが全部入る
ほかのゲームの AI (「AI と対戦」で選べる)full: false。何も無い盤面が入る。読めるのは、自分と同じゲームの盤面だけです

入力の書き方

書き方は3通りあり、混ぜてもかまいません。

書き方意味
['left'] / 'left'押してすぐ離す = 1回ぶん。ふつうはこれで足りる
['soft', 1]押したままにする
['soft', 0]離す
{a:'left', d:1}上と同じ。d を省くと1回ぶん
    // 1フレームぶんをまとめて送ってよい。 この順に反映される
    CH.postMessage({ from:'ai', act: [ ['cw'], ['left'], ['left'], ['hard'] ] });
  • 横の1回押し (['left'] ['right']) は、その場で1マス動きます
  • 1フレームで反映するのは 64個までです。それを大きく超えて溜まると、追いつけないと見なして列ごと捨てます
  • 知らない名前の操作は、黙って無視されます

操作の名前

名前動き
left / right横へ1マス
cw / ccw右回転 / 左回転
softソフトドロップ。['soft', 1] で押したまま、['soft', 0] で離す
hardハードドロップ。その場で置かれる
holdホールド。1組につき1回

回転の決まり

  • 回した先が壁やブロックでふさがっていたら、軸が子の反対側へ1マスずれて回ります。子を下へ回す時は、1段持ち上がります
  • それも入らない時 (両側がふさがった縦1列のすき間) は、quickTurnF のうちに回転を2回押すと半回転して、軸と子の上下が入れ替わります。1回目は動きません
  • 持ち上がる回転は1組につき liftMax 回までです。使い切ったあとに持ち上げようとすると、その場で置かれます

操作できない間に送ったもの

canAct が false の間 (連鎖の最中・次の組を待っている間) に送った回転とホールドは、次の組が出た瞬間に効きます (先行入力)。 横移動とハードドロップは、その間は何も起きません。考えるのは canAct が true になってからにすると、取り違えがありません。


盤面の決まりごと (meta)

hello に乗ってきます。時間の単位はすべてフレーム (1/60秒) です。

キー値意味
game'paya'ゲームの種別
w / h6 / 12盤面の 幅 / 高さ
rows13board に入っている段数 (h + 13段目)
fps601秒あたりのフレーム数
bottomFirsttrueboard が下の段から並んでいる
pieceTop13操作している組が上がれる、いちばん上の y (14段目。画面には見えない)
colors4色の数。盤面では 1〜4
garb7おじゃまの番号
clearN4同じ色がこの数つながると消える
spawn{x:2, y:11, o:0}組が出てくる所 (軸)。ここが埋まると負け
dirs[[0,1],[1,0],[0,-1],[-1,0]]向き o ごとの、軸から見た子の位置
actionsstring[7]送れる操作の名前
nextLen3見せている NEXT の数
chainPownumber[16]連鎖ごとの倍率 (中身は「得点とおじゃま」に)
colorBonusnumber[6]同時に消した色の数の上乗せ
groupBonusnumber[12]まとまりの大きさの上乗せ
target120おじゃま1個ぶんの得点
allClearAtk / allClearPts18 / 2100全消しで上乗せされる おじゃま / 得点
garbMax301手で降るおじゃまの最大 (5段ぶん)
garbWaitF180おじゃまが確定してから、降れるようになるまで (3秒)
lockF36接地してから置かれるまで。ソフトドロップ中は4倍の速さで進む
lockResetMax15接地中に動かして、置かれるのを先延ばしできる回数
liftMax8回転で1段持ち上がれる回数 (1組あたり)
quickTurnF24両側がふさがっている時、この間に回転を2回押すと半回転する
areF8連鎖 (とおじゃま) が終わってから、次の組が出るまで
fallF4ちぎれ・連鎖・おじゃまで1段落ちる時間
landF7落ちきってから、消える判定まで
clearF56消える演出
softF2ソフトドロップで1段落ちる時間
dasF / arrF9 / 2押しっぱなしの横移動の 溜め / 連射 (プレイヤーの設定で変わる)
gravobject落下速度の決まり (後述)
marginobject長引いた試合の底上げの決まり (後述)
chars / charobject[] / string選べるキャラクター [{id,name}] / いま選ばれている id

盤面の読み方

board は 13段 × 6列 = 78文字です。下の段から、左から右へ並びます。

    index = y * 6 + x        // 0 ≦ x < 6,  0 ≦ y < 13。 y = 0 がいちばん下
    cell  = +board[index]
  • y = 0〜11 … 見えている12段
  • y = 12 … 13段目。置けますが、ここにあるものは消えません (つながりに数えません)
  • y = 13 から上 (14段目より上) … board には入りません。置いたものが落ちきった時に、ここに残っているものは消えます

マスの数字

01〜47
空色おじゃま

おじゃまは、となり (上下左右) で色が消えると一緒に消えます。

cur の座標

操作している組は board には入らず、cur で届きます。組は軸と子の2つでできています。

    cur: { x: 2, y: 11, o: 0,            // 軸の位置と、向き
           a: 3, b: 1,                   // 軸の色 / 子の色
           cells: [[2, 11], [2, 12]],    // いまの位置 [軸, 子]
           ghost: [[2, 0], [2, 1]],      // まっすぐ落とした時に落ち着く所 [軸, 子]
           grounded: false }             // 接地しているか
o0123
子の位置軸の上軸の右軸の下軸の左

cw で 0 → 1 → 2 → 3、ccw でその逆に回ります。子の位置は meta.dirs[o] を軸に足せば出ます。

横向きで置いた時、左右の列の高さが違えば、軸と子は別々の高さまで落ちます (ちぎれ)。ghost はそれぞれが落ちきった所です。


state (me / opp)

キー型意味
boardstring(78)積まれているものだけ。操作している組は入らない
curobject / null操作している組。操作できない間は null
holdnumber[2] / nullホールドしてある組 [軸の色, 子の色]
holdUsedboolこの組でもうホールドを使ったか
nextnumber[3][2]次に来る組 [[軸の色, 子の色], …]。画面の NEXT と同じ
scorenumber得点
clearednumber消した数の合計
placednumber置いた組の数
aliveboolまだ生きているか
chainnumberいま何連鎖目か。連鎖していなければ 0
chainTotalnumber置いた手が、このまま何連鎖になるか (名前の右下に出ている数)
lastChain / maxChainnumber直前の連鎖数 / この試合の最大連鎖数
allClearbool全消しの上乗せ (allClearAtk) が、次の連鎖に付くか
clearingobject / null消えている最中のもの { cells: [[x,y], …], garb: [[x,y], …] }
garbagenumber確定して、降るのを待っているおじゃまの数 (予告で、はっきり見えているぶん)
incomingnumber相手の連鎖がまだ続いていて、確定していない数 (点滅しているぶん)
previewnumber相手が連鎖の途中で送りかけている数 (同じく点滅)
garbReadynumberいま降れる数 (赤く光っているぶん)
garbColsnumber[6] / null6個に満たない端数が降る列の順。予告が出ていなければ null
sendingnumber自分の連鎖がまだ続いていて、相手へ確定していないおじゃまの数
levelnumber落下速度の段階 (1〜20)
gravityobject{ framesPerRow } = 1段落ちるのにかかるフレーム数
phstringいまの段階 (下の表)
phasestring'play' 操作中 / 'wait' 連鎖やおじゃまの最中 / 'dead'
canActboolいま送った操作が効くか
curIdnumber組が変わるたびに変わる番号。ホールドで入れ替えた時も変わる

ph の中身

ph意味
'ready'始まる前
'play'操作中
'fall'置いたもの・ちぎれたもの・おじゃまが落ちている
'check'消えるかどうかの判定
'clear'消えている
'are'次の組を待っている
'dead'負けた

match (試合全体)

キー意味
frame試合が進んだフレーム数
vsMsマージンを数えている経過時間 (ミリ秒)
margin.levelマージンの、いまの段階
margin.targetいまの、おじゃま1個ぶんの得点

得点とおじゃま

    1回ぶんの得点 = 10 × 消した数 × 倍率
    倍率          = chainPow[連鎖 - 1] + colorBonus[同時に消した色の数] + Σ groupBonus[まとまりの個数]
                    (1〜999 に収める)
    おじゃまの数  = 得点 ÷ target          // 対戦中は match.margin.target で割る

    chainPow   = [4, 12, 24, 32, 48, 96, 160, 240, 320, 400, 500, 600, 700, 800, 900, 999]
    colorBonus = [0, 0, 2, 4, 8, 16]                      // 添字 = 色の数
    groupBonus = [0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 8]     // 添字 = 個数 (11個以上は 8)
  • 端数は切り捨てず、次へ持ち越します。 いくつ持ち越しているかは渡していません
  • 連鎖が chainPow の長さを超えたら、最後の値 (999) がそのまま使われます
  • 4個を1色で消した1連鎖目は 10 × 4 × 4 = 160点で、おじゃま 1.33個ぶん。2連鎖目に同じく4個消すと 10 × 4 × 12 = 480点で、4個ぶんです
  • 全消しは得点に allClearPts が入り、次の連鎖の1回目におじゃま allClearAtk 個が上乗せされます (me.allClear が true の間)
  • ハードドロップは落ちた段数、ソフトドロップは1段ごとに 1点です

おじゃまが降るまで

盤面の上に出ている予告と、同じ中身が届きます。

  • 相手が連鎖を始めると、送られてくるぶんが incoming に入ります (予告は点滅)
  • 相手の連鎖が終わると確定して、garbage に移ります
  • 確定から garbWaitF (3秒) たつと、garbReady に数えられます (予告が赤く光る)
  • 降るのは、1つも消さずに組を置いた手のあとだけです。1手に garbMax 個までで、残りは次の手に回ります
  • 6個で1段ぶん。6個に満たない端数は、garbCols の先頭から順の列に降ります

自分の攻撃は、まず自分が受けているぶんを打ち消します (新しいものから)。余ったぶんだけが相手へ飛びます。 消している手ではおじゃまが降らないので、消し続けているかぎり garbReady が残っていても降ってきません。

落下速度 (meta.grav)

    level        = min(maxLevel, 1 + floor(placed / placedPerLevel))
    framesPerRow = max(minF, round(baseF × decay^(level - 1)))   // 1段落ちるのにかかるフレーム数
キー値意味
baseF40レベル1で1段落ちるフレーム数
decay0.86レベルが1上がるごとに掛かる倍率
minF2これより速くはならない
placedPerLevel12この数の組を置くごとにレベルが1上がる
maxLevel20レベルの上限

いまの値は、毎フレームの me.level と me.gravity.framesPerRow に入っています。

長引いた試合の底上げ (meta.margin)

対戦が長引くと段階が上がり、おじゃま1個ぶんの得点が下がります (同じ連鎖で、たくさん送れるようになる)。

    段階 = 経過 < startMs ? 0 : 1 + floor((経過ミリ秒 - startMs) / stepMs)     // target の最後まで
キー値意味
startMs96000底上げが始まるまでの時間 (96秒)
stepMs160001段階すすむ時間 (16秒)
target[120,90,67,50,38,30]段階ごとの、おじゃま1個ぶんの得点

いまの段階と得点は match.margin に入っているので、式を自分で追う必要はありません。


キャラクターと名前

盤面の背景になるキャラクターと、盤面の下に出る名前は、AI 側から指定できます。どちらもどのメッセージに添えてもかまいません。

やり方いつ効くか
{ from:'ai', type:'pong', name:'わたしのAI', game:'paya' }名前。16文字まで
{ from:'ai', type:'pong', char:'c3', game:'paya' }キャラクター。次に始める試合から
{ from:'ai', type:'char', id:'c3' }いつ送ってもよい。試合中に送ると次の試合から

選べる id は meta.chars に一覧で入っています。知らない id は黙って無視されます。名乗らなかった場合は「あなたの AI」と出ます。


対戦相手として投稿する

自分の AI を、ほかの人が選べる対戦相手として上げることができます。AI DEVELOP の アップロード準備 から進みます。

関門は、PAYA BLOCKS 用の AI Carbunfly に10連勝することです。

  • 「ファイルをえらぶ」で選んだ AI が動いていること、ファイルが 500KB 以下であることが条件です
  • 10戦ともふつうの対戦で、1敗したらはじめからやり直しです
  • 抜けると名前 (16文字まで) と作った人 (12文字まで) を入れる画面になり、アップロードする を押すと「AI と対戦」の一覧に並びます
  • 同じ名前の AI がすでにある時は、上書きするかどうかを聞かれます。上書きしても、レートと成績は引き継がれます
  • 上げたあとは、AI DEVELOP の 投稿済み AI の 調整する から、名前や備考を直したり消したりできます

上げた AI は、同じファイルのまま相手役としても動きます。その時は me が自分 (相手役)、opp がプレイヤーの盤面です (プレイヤーがほかのゲームで遊んでいる時は、opp は何も無い盤面になります)。 相手役の時は ping が来ません。起動した時の hello に game: 'paya' を添えておけば、それで種別が伝わります。


つまずきやすい所

  • 返事は待ちません。 入力は届いた次のフレームの頭で反映されるので、実際には 1〜2フレーム遅れて入ります。そのぶん AI が重くても画面は止まりません
  • canAct を見てから送ってください。 操作できない間に送った回転とホールドが、次の組に先行入力として効いてしまいます
  • y = 0 がいちばん下です。 TRISBLOCKS や MAGI BLOCKS とは上下が逆なので、ほかのゲームの AI を写す時は気を付けてください
  • 負けは1マスだけです。spawn (左から3列目の12段目) が埋まった時だけ負けで、ほかの列は13段目まで積んでも負けません
  • おじゃまは、消さずに置いた手のあとにしか降りません。 garbReady が溜まっている時こそ、消せる手を優先する価値があります
  • 13段目は消えません。 そこに置いた色は、下が消えて落ちてくるまで、つながりに数えられません
  • hello と pong の両方に game: 'paya' を。 片方だけだと「読み込み失敗」になります

ほかのゲームの API は、MAGI BLOCKS ② API一覧 と trisblocks ② API一覧 にあります。