このメモについて
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 用の AI | full: 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 / h | 6 / 12 | 盤面の 幅 / 高さ |
rows | 13 | board に入っている段数 (h + 13段目) |
fps | 60 | 1秒あたりのフレーム数 |
bottomFirst | true | board が下の段から並んでいる |
pieceTop | 13 | 操作している組が上がれる、いちばん上の y (14段目。画面には見えない) |
colors | 4 | 色の数。盤面では 1〜4 |
garb | 7 | おじゃまの番号 |
clearN | 4 | 同じ色がこの数つながると消える |
spawn | {x:2, y:11, o:0} | 組が出てくる所 (軸)。ここが埋まると負け |
dirs | [[0,1],[1,0],[0,-1],[-1,0]] | 向き o ごとの、軸から見た子の位置 |
actions | string[7] | 送れる操作の名前 |
nextLen | 3 | 見せている NEXT の数 |
chainPow | number[16] | 連鎖ごとの倍率 (中身は「得点とおじゃま」に) |
colorBonus | number[6] | 同時に消した色の数の上乗せ |
groupBonus | number[12] | まとまりの大きさの上乗せ |
target | 120 | おじゃま1個ぶんの得点 |
allClearAtk / allClearPts | 18 / 2100 | 全消しで上乗せされる おじゃま / 得点 |
garbMax | 30 | 1手で降るおじゃまの最大 (5段ぶん) |
garbWaitF | 180 | おじゃまが確定してから、降れるようになるまで (3秒) |
lockF | 36 | 接地してから置かれるまで。ソフトドロップ中は4倍の速さで進む |
lockResetMax | 15 | 接地中に動かして、置かれるのを先延ばしできる回数 |
liftMax | 8 | 回転で1段持ち上がれる回数 (1組あたり) |
quickTurnF | 24 | 両側がふさがっている時、この間に回転を2回押すと半回転する |
areF | 8 | 連鎖 (とおじゃま) が終わってから、次の組が出るまで |
fallF | 4 | ちぎれ・連鎖・おじゃまで1段落ちる時間 |
landF | 7 | 落ちきってから、消える判定まで |
clearF | 56 | 消える演出 |
softF | 2 | ソフトドロップで1段落ちる時間 |
dasF / arrF | 9 / 2 | 押しっぱなしの横移動の 溜め / 連射 (プレイヤーの設定で変わる) |
grav | object | 落下速度の決まり (後述) |
margin | object | 長引いた試合の底上げの決まり (後述) |
chars / char | object[] / 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には入りません。置いたものが落ちきった時に、ここに残っているものは消えます
マスの数字
| 0 | 1〜4 | 7 |
|---|---|---|
| 空 | 色 | おじゃま |
おじゃまは、となり (上下左右) で色が消えると一緒に消えます。
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 } // 接地しているか
o | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 子の位置 | 軸の上 | 軸の右 | 軸の下 | 軸の左 |
cw で 0 → 1 → 2 → 3、ccw でその逆に回ります。子の位置は meta.dirs[o] を軸に足せば出ます。
横向きで置いた時、左右の列の高さが違えば、軸と子は別々の高さまで落ちます (ちぎれ)。ghost はそれぞれが落ちきった所です。
state (me / opp)
| キー | 型 | 意味 |
|---|---|---|
board | string(78) | 積まれているものだけ。操作している組は入らない |
cur | object / null | 操作している組。操作できない間は null |
hold | number[2] / null | ホールドしてある組 [軸の色, 子の色] |
holdUsed | bool | この組でもうホールドを使ったか |
next | number[3][2] | 次に来る組 [[軸の色, 子の色], …]。画面の NEXT と同じ |
score | number | 得点 |
cleared | number | 消した数の合計 |
placed | number | 置いた組の数 |
alive | bool | まだ生きているか |
chain | number | いま何連鎖目か。連鎖していなければ 0 |
chainTotal | number | 置いた手が、このまま何連鎖になるか (名前の右下に出ている数) |
lastChain / maxChain | number | 直前の連鎖数 / この試合の最大連鎖数 |
allClear | bool | 全消しの上乗せ (allClearAtk) が、次の連鎖に付くか |
clearing | object / null | 消えている最中のもの { cells: [[x,y], …], garb: [[x,y], …] } |
garbage | number | 確定して、降るのを待っているおじゃまの数 (予告で、はっきり見えているぶん) |
incoming | number | 相手の連鎖がまだ続いていて、確定していない数 (点滅しているぶん) |
preview | number | 相手が連鎖の途中で送りかけている数 (同じく点滅) |
garbReady | number | いま降れる数 (赤く光っているぶん) |
garbCols | number[6] / null | 6個に満たない端数が降る列の順。予告が出ていなければ null |
sending | number | 自分の連鎖がまだ続いていて、相手へ確定していないおじゃまの数 |
level | number | 落下速度の段階 (1〜20) |
gravity | object | { framesPerRow } = 1段落ちるのにかかるフレーム数 |
ph | string | いまの段階 (下の表) |
phase | string | 'play' 操作中 / 'wait' 連鎖やおじゃまの最中 / 'dead' |
canAct | bool | いま送った操作が効くか |
curId | number | 組が変わるたびに変わる番号。ホールドで入れ替えた時も変わる |
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段落ちるのにかかるフレーム数
| キー | 値 | 意味 |
|---|---|---|
baseF | 40 | レベル1で1段落ちるフレーム数 |
decay | 0.86 | レベルが1上がるごとに掛かる倍率 |
minF | 2 | これより速くはならない |
placedPerLevel | 12 | この数の組を置くごとにレベルが1上がる |
maxLevel | 20 | レベルの上限 |
いまの値は、毎フレームの me.level と me.gravity.framesPerRow に入っています。
長引いた試合の底上げ (meta.margin)
対戦が長引くと段階が上がり、おじゃま1個ぶんの得点が下がります (同じ連鎖で、たくさん送れるようになる)。
段階 = 経過 < startMs ? 0 : 1 + floor((経過ミリ秒 - startMs) / stepMs) // target の最後まで
| キー | 値 | 意味 |
|---|---|---|
startMs | 96000 | 底上げが始まるまでの時間 (96秒) |
stepMs | 16000 | 1段階すすむ時間 (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一覧 にあります。