IchigoJam でコラムス風ゲーム

[IchigoJam](https://ichigojam.net/) で[コラムス](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%A9%E3%83%A0%E3%82%B9)風のゲームを作ってみた。

※IchigoJamはjig.jpの登録商標です。

# ルール

フィールド内に6種類のブロックのうち3個が縦に連なって落ちてくる。
位置や順番を調整して並べ、縦・横・斜めのいずれかに同じ種類のブロックが3個以上並ぶと消える。
ブロックを置いたとき、フィールドからはみ出していたらゲームオーバー。
何個のブロックを消せたかをカウントする。

連鎖によるボーナス点や、消したブロック数による落下の加速などは無い。

# プログラム

[OneFiveCrowd で実行する](https://onefivecrowd.mikecat.work/#text=LRUN100%0A&en=%E3%82%B3%E3%83%A9%E3%83%A0%E3%82%B9%E9%A2%A8&e0=CgAMJyC619G9IMyzIDEAABQAEklGWj0xNjc5Wj0wOkdPVE81MAAeAMRDTFM6TEMxMiwyOkZPUkk9MVRPMTA6P0NIUiQoNik7Ok5FWFQ6Rk9SST0zVE8xNjpMQzEyLEk6P0NIUiQoNik7OkxDMTksSTo_Q0hSJCg2KTs6TkVYVDpMQzEzLDE2Oj9DSFIkKDYsNiw2LDYsNiw2KTs6TEMyMCw2Oj9DSFIkKDYpOzpGT1JJPTNUTzY6TEMyMSxJOj9DSFIkKDYpOzpORVhUOkxFVFswXSwwLCNFQiwjRUMsI0VFLCNFRiwjRjUsI0ZGACgAbEZPUkk9MFRPNjpGT1JKPTBUTzc6UE9LRSM3MDArSSo4K0osUEVFSyg4KltJXStKKTpORVhUOk5FWFQ6Q0xWOkZPUkk9MFRPMjpbNStJXT1STkQoNikrMTpORVhUOkI9MDpMQzI2LDI6PzA7AAAyAGxGT1JJPTBUTzI6WzErSV09WzUrSV06WzUrSV09Uk5EKDYpKzE6TEMyMCw1LUk6P0NIUiQoI0UwK1s1K0ldKTs6TkVYVDpXPTM6WD0zOlk9LTE6Q0xUOkw9LTU6Uj0tNTpEPS01OkM9MDpHPTAAPABORk9SST0wVE8zOklGWS1JPj0wTEMxMytYLDMrWS1JOj9DSFIkKCNFMCtbMStJXSk7OklGVy1YTEMxMytXLDMrWS1JOj9DSFIkKCNFMCk7AEYADE5FWFQ6TEMwLDE3AABQAFRXPVg6VD1USUNLKCk6Qz1TOlM9QlROKFNQQUNFKTpJRkJUTihMRUZUKUlGVC1MPjRJRlhQPTgrWDpHU0IxNTA6SUZWWD1YLTE6TD1UOkdPVE82MAAAWgA6SUZCVE4oUklHSFQpSUZULVI-NElGWDw1UD0xMCtYOkdTQjE1MDpJRlZYPVgrMTpSPVQ6R09UTzYwAABkAC5JRlMqIUNGT1JJPTBUTzI6W0ldPVtJKzFdOk5FWFQ6WzNdPVswXTpHT1RPNjAAAG4ANklGQlROKERPV04pQU5EVC1EPjREPVRFTFNFSUZULUc-MjlHPVRFTFNFV0FJVDE6R09UTzgwAAB4ACBJRlk8MTJJRiFbMTYrNypZK1hdWT1ZKzE6R09UTzYwAACCADJJRlk8MkxDMTIsMTk6PyJHQU1FIE9WRVIiOlBMQVkiTDE2Qz5CQUdGNCI6R09UTzE3MACMABRaPTUxODI6TFJVTkZJTEUoKSsxAACWACZWPTE6Rk9SST0wVE8yOklGWS1JPj0wSUZbUCs3KihZLUkpXVY9MACgAAhORVhUOlJUTgCqAB5JRklOS0VZKCk9MTBHT1RPMzBFTFNFR09UTzE3MA&e1=yAAMJyC619G9IMyzIDIAANIAFElGWi01MTgyTFJVTkZJTEUoKS0xANwAKlo9MDpGPTE6Rk9SST0wVE8yOls5KzcqKFktSSkrWF09WzErSV06TkVYVADmADBFPTA6Rk9SWT0wVE8xMjpGT1JYPTBUTzU6Qz05KzcqWStYOklGIVtDXUdPVE8yODAA8AA4SUZbQy0xXT1bQ11JRltDXT1bQysxXUU9MTpMQzEyK1gsMytZOj9DSFIkKCNGNCwjRjQsI0Y0KTsA-gASSUZZPTBPUlk9MTJHT1RPMjgwAAQBVkZPUkQ9LTFUTzE6SUZbQy03LURdPVtDXUlGW0NdPVtDKzcrRF1FPTE6Rk9SRT0tMVRPMTpMQzEzK1grRCpFLDMrWStFOj9DSFIkKCNGNCk7Ok5FWFQAAA4BBE5FWFQAGAEuTkVYVDpORVhUOklGRiohRVBMQVkiPkYxNi4iRUxTRUlGRVBMQVkiTDE2R0IuIgAiAQxJRkUrRldBSVQzMAAALAEMSUYhRUdPVE8zNTAAADYBeEY9MDpGT1JYPTBUTzU6UD0xMzpGT1JZPTEyVE8wU1RFUC0xOkM9U0NSKDEzK1gsMytZKTpJRkM9I0Y0OkI9QisxRUxTRVA9UC0xOklGUD5ZTEMxMytYLDMrUDo_Q0hSJChDKTs6WzkrNypQK1hdPVs5KzcqWStYXQBAAUBORVhUOklGUEZPUkk9UC0xVE8wU1RFUC0xOkxDMTMrWCwzK0k6P0NIUiQoMCk7Ols5KzcqSStYXT0wOk5FWFQAAEoBFE5FWFQ6SUZCPjk5OTlCPTk5OTkAAFQBIkxDMjMsMjo_REVDJChCLDQpOzpXQUlUMzA6R09UTzIzMAAAXgEUWj0xNjc5OkxSVU5GSUxFKCktMQ&font=1_4)

「描画・操作」のプログラムの次のファイル番号に「消去判定」のプログラムを保存した状態で実行すること。
どっちのプログラムから実行してもよい。

## 描画・操作

```
10 ' ｺﾗﾑｽ ﾌｳ 1
20 IFZ=1679Z=0:GOTO50
30 CLS:LC12,2:FORI=1TO10:?CHR$(6);:NEXT:FORI=3TO16:LC12,I:?CHR$(6);:LC19,I:?CHR$(6);:NEXT:LC13,16:?CHR$(6,6,6,6,6,6);:LC20,6:?CHR$(6);:FORI=3TO6:LC21,I:?CHR$(6);:NEXT:LET[0],0,#EB,#EC,#EE,#EF,#F5,#FF
40 FORI=0TO6:FORJ=0TO7:POKE#700+I*8+J,PEEK(8*[I]+J):NEXT:NEXT:CLV:FORI=0TO2:[5+I]=RND(6)+1:NEXT:B=0:LC26,2:?0;
50 FORI=0TO2:[1+I]=[5+I]:[5+I]=RND(6)+1:LC20,5-I:?CHR$(#E0+[5+I]);:NEXT:W=3:X=3:Y=-1:CLT:L=-5:R=-5:D=-5:C=0:G=0
60 FORI=0TO3:IFY-I>=0LC13+X,3+Y-I:?CHR$(#E0+[1+I]);:IFW-XLC13+W,3+Y-I:?CHR$(#E0);
70 NEXT:LC0,17
80 W=X:T=TICK():C=S:S=BTN(SPACE):IFBTN(LEFT)IFT-L>4IFXP=8+X:GSB150:IFVX=X-1:L=T:GOTO60
90 IFBTN(RIGHT)IFT-R>4IFX<5P=10+X:GSB150:IFVX=X+1:R=T:GOTO60
100 IFS*!CFORI=0TO2:[I]=[I+1]:NEXT:[3]=[0]:GOTO60
110 IFBTN(DOWN)ANDT-D>4D=TELSEIFT-G>29G=TELSEWAIT1:GOTO80
120 IFY<12IF![16+7*Y+X]Y=Y+1:GOTO60
130 IFY<2LC12,19:?"GAME OVER":PLAY"L16C>BAGF4":GOTO170
140 Z=5182:LRUNFILE()+1
150 V=1:FORI=0TO2:IFY-I>=0IF[P+7*(Y-I)]V=0
160 NEXT:RTN
170 IFINKEY()=10GOTO30ELSEGOTO170
```

## 消去判定

```
200 ' ｺﾗﾑｽ ﾌｳ 2
210 IFZ-5182LRUNFILE()-1
220 Z=0:F=1:FORI=0TO2:[9+7*(Y-I)+X]=[1+I]:NEXT
230 E=0:FORY=0TO12:FORX=0TO5:C=9+7*Y+X:IF![C]GOTO280
240 IF[C-1]=[C]IF[C]=[C+1]E=1:LC12+X,3+Y:?CHR$(#F4,#F4,#F4);
250 IFY=0ORY=12GOTO280
260 FORD=-1TO1:IF[C-7-D]=[C]IF[C]=[C+7+D]E=1:FORE=-1TO1:LC13+X+D*E,3+Y+E:?CHR$(#F4);:NEXT
270 NEXT
280 NEXT:NEXT:IFF*!EPLAY">F16."ELSEIFEPLAY"L16GB."
290 IFE+FWAIT30
300 IF!EGOTO350
310 F=0:FORX=0TO5:P=13:FORY=12TO0STEP-1:C=SCR(13+X,3+Y):IFC=#F4:B=B+1ELSEP=P-1:IFP>YLC13+X,3+P:?CHR$(C);:[9+7*P+X]=[9+7*Y+X]
320 NEXT:IFPFORI=P-1TO0STEP-1:LC13+X,3+I:?CHR$(0);:[9+7*I+X]=0:NEXT
330 NEXT:IFB>9999B=9999
340 LC23,2:?DEC$(B,4);:WAIT30:GOTO230
350 Z=1679:LRUNFILE()-1
```

## ライセンス

このプログラムは、[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja) でライセンスする。
このプログラム (改造したものを含む) を公開の場で利用する際は、出典を示していただけると嬉しい。
これは、Qiitaの利用規約に基づくプログラムの利用を禁止するものではない。

# 操作方法

* 左矢印キー：落ちてくるブロックを左に動かす
* 右矢印キー：落ちてくるブロックを右に動かす
* 下矢印キー：落ちてくるブロックを速く落とす
* スペースキー：落ちてくるブロックの順番を変える
* Enterキー：ゲームオーバー時に次のゲームを始める

[HetaPad](https://kitaharima.pcn.club/index.php?aid=posts&mid=hetapad) での操作にも対応している。

# 実行結果例

<iframe width="640" height="480" src="https://www.youtube.com/embed/gkxyV9-ec0E?si=Dpz6qDuQHwSM0USJ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# 解説

## 描画・操作

### 10行目：タイトル

`FILES` 対応のタイトル行。

### 20行目：連携判定

変数 `Z` の値が特定の値である場合、「消去判定」のプログラムから戻ってきたとみなし、初期化処理を飛ばしてゲームを進める。

### 30～40行目：初期化

> ```
> 30 CLS:LC12,2:FORI=1TO10:?CHR$(6);:NEXT
> FORI=3TO16:LC12,I:?CHR$(6);:LC19,I:?CHR$(6);:NEXT
> LC13,16:?CHR$(6,6,6,6,6,6);:LC20,6:?CHR$(6);
> FORI=3TO6:LC21,I:?CHR$(6);:NEXT
> ```

フィールドの外枠を描画する。

> ```
> LET[0],0,#EB,#EC,#EE,#EF,#F5,#FF
> ```

使用するブロックに対応するキャラクターコードを定義する。

> ```
> 40 FORI=0TO6:FORJ=0TO7:POKE#700+I*8+J,PEEK(8*[I]+J):NEXT:NEXT
> ```

描画を単純化するため、ブロックとして用いるキャラクターパターンを順に並べる。

> ```
> CLV:FORI=0TO2:[5+I]=RND(6)+1:NEXT
> ```

最初に落下させるブロックを選ぶ。

> ```
> B=0:LC26,2:?0;
> ```

ブロックの消去数を初期化し、描画する。

### 50行目：落下開始処理

> ```
> 50 FORI=0TO2:[1+I]=[5+I]:[5+I]=RND(6)+1:LC20,5-I:?CHR$(#E0+[5+I]);:NEXT
> ```

次に落下させるブロック (`[5]`～`[7]`) を落下中のブロック (`[1]`～`[3]`) に設定する。
次に落下させるブロックを選択し、描画する。

> ```
> W=3:X=3:Y=-1:CLT:L=-5:R=-5:D=-5:C=0:G=0
> ```

以下の変数を初期化する。

|変数|意味|
|:-:|---|
|W|前回落下中のブロックがあったx座標|
|X|現在落下中のブロックがあるx座標|
|Y|現在落下中のブロックのうち一番下のものがあるy座標|
|L|落下中のブロックを前回左に動かした時刻|
|R|落下中のブロックを前回右に動かした時刻|
|D|落下中のブロックを前回操作により下に動かした時刻|
|C|前回位置変え操作がされていたか|
|G|落下中のブロックを前回自動で下に動かした時刻|

### 60～70行目：描画処理

> ```
> 60 FORI=0TO3
> ```

落下中の各ブロック、およびブロックの1個上のスペースについて描画を行う。

> ```
> IFY-I>=0LC13+X,3+Y-I:?CHR$(#E0+[1+I]);
> ```

ブロックがフィールド内にある場合、描画を行う。
ブロックが下に動いた場合を考え、0 が格納されている `[4]` を用いた描画されたブロックの消去も行う。

> ```
> IFW-XLC13+W,3+Y-I:?CHR$(#E0);
> ```

ブロックが横に動いた場合、前の位置に描画されているブロックを消去する。

> ```
> 70 NEXT:LC0,17
> ```

ループを進める。
ループ後、プログラムの実行停止に備え、カーソルをフィールドの下に移動する。

### 80～160行目：操作・落下処理

> ```
> 80 W=X:T=TICK():C=S:S=BTN(SPACE)
> ```

操作前のブロックの位置、現在時刻、位置替え操作の前の状態、位置変え操作の今の状態をそれぞれ取得する。

> ```
> IFBTN(LEFT)IFT-L>4IFXP=8+X:GSB150:IFVX=X-1:L=T:GOTO60
> ```

以下の条件をすべて満たす場合、ブロックを左に動かし、左移動を行った時刻を更新し、描画処理に戻る。

|条件判定|意味|
|---|---|
|`IFBTN(LEFT)`|左矢印キーが押されている|
|`IFT-L>4`|前回の左移動から5フレーム以上経過している|
|`IFX`|ブロックの位置が左端ではない|
|`P=8+X:GSB150:IFV`|ブロックの左移動を妨げる位置にブロックが積まれていない|

> ```
> 90 IFBTN(RIGHT)IFT-R>4IFX<5P=10+X:GSB150:IFVX=X+1:R=T:GOTO60
> ```

以下の条件をすべて満たす場合、ブロックを右に動かし、右移動を行った時刻を更新し、描画処理に戻る。

|条件判定|意味|
|---|---|
|`IFBTN(RIGHT)`|左矢印キーが押されている|
|`IFT-R>4`|前回の右移動から5フレーム以上経過している|
|`IFX<5`|ブロックの位置が右端ではない|
|`P=10+X:GSB150:IFV`|ブロックの右移動を妨げる位置にブロックが積まれていない|

> ```
> 100 IFS*!CFORI=0TO2:[I]=[I+1]:NEXT:[3]=[0]:GOTO60
> ```

前回位置変えボタンが押されておらず、今回押されている場合、ブロックの配置をローテートし、描画処理に戻る。

> ```
> 110 IFBTN(DOWN)ANDT-D>4D=TELSEIFT-G>29G=TELSEWAIT1:GOTO80
> ```

以下のいずれかの条件が満たされている場合、対応する時刻を更新し、次 (落下処理) に進む。
そうでない場合、入力の受付に戻る。

|条件判定|意味|
|---|---|
|`IFBTN(DOWN)ANDT-D>4`|下矢印キーが押されており、<br>かつ前回の操作による下移動から5フレーム以上経過している|
|`IFT-G>29`|前回の自動での下移動から30フレーム以上経過している|

> ```
> 120 IFY<12IF![16+7*Y+X]Y=Y+1:GOTO60
> ```

以下の条件をすべて満たす場合、ブロックを下に動かし、描画処理に戻る。
そうでない場合、次 (ブロックの配置) に進む。

|条件判定|意味|
|---|---|
|`IFY<12`|ブロックの下がフィールドの底についていない|
|`IF![16+7*Y+X]`|ブロックの下が積まれたブロックについていない|

> ```
> 130 IFY<2LC12,19:?"GAME OVER":PLAY"L16C>BAGF4":GOTO170
> ```

ブロックが着地したとき、ブロックがフィールドの上にはみ出しているならば、ゲームオーバーとして、ゲームオーバーの表示と音の再生を行う。
今回の仕様では、着地した後3個以上並んだブロックを消去すればはみ出しが解消する場合でも、着地した時点ではみ出しているならゲームオーバーである。

> ```
> 140 Z=5182:LRUNFILE()+1
> ```

消去判定のプログラムを実行する。

> ```
> 150 V=1:FORI=0TO2:IFY-I>=0IF[P+7*(Y-I)]V=0
> 160 NEXT:RTN
> ```

ブロックの横に積まれたブロックがあるかの判定を行う。
ない場合は変数 `V` に 1、ある場合は 0 を格納して帰る。

### 170行目：次ゲーム待機

> ```
> 170 IFINKEY()=10GOTO30ELSEGOTO170
> ```

ゲームオーバー時、ここを実行する。
Enterキーが入力されたら、初期化処理に進み、次のゲームを開始する。
そうでない間、ここを繰り返し実行する。

## 消去判定

`LRUN` を `GOTO` に置き換えることで IchigoCake に移植しやすいよう、「描画・操作」のプログラムとは行番号が重ならないようにした。

### 200行目：タイトル

`FILES` 対応のタイトル行。

### 210行目：連携判定

変数 `Z` の値が特定の値でない場合、直接実行されたとみなし、「描画・操作」のプログラムを最初 (初期化処理) から実行する。

### 220行目：ブロックの配置

> ```
> 220 Z=0:F=1
> ```

連携用変数 `Z` をクリアする。
初判定フラグ `F` を立てる。

> ```
> FORI=0TO2:[9+7*(Y-I)+X]=[1+I]:NEXT
> ```

落下してきたブロックをフィールドに積む。
フィールドは、1行を横6列に番兵を加えた7要素で表現する。

### 230～280行目：消去判定

> ```
> 230 E=0
> ```

消去フラグ `E` を折る。

> ```
> FORY=0TO12:FORX=0TO5
> ```

フィールド内の各行・各列について、それぞれを中心とする判定を行う。

> ```
> C=9+7*Y+X:IF![C]GOTO280
> ```

現在の座標に対応する配列の添字を変数 `C` にセットする。
中心にブロックがない場合、消去は起こり得ないので、次の場所の処理に進む。

> ```
> 240 IF[C-1]=[C]IF[C]=[C+1]E=1:LC12+X,3+Y:?CHR$(#F4,#F4,#F4);
> ```

横に3個同じブロックが並んでいる場合、消去フラグを立て、それらを消去する (画面に爆発パターンを書き込む)。
画面に書き込まれた爆発パターンは、後の処理で消去対象フラグとして用いる。

> ```
> 250 IFY=0ORY=12GOTO280
> ```

現在の座標が一番上または一番下の行であるならば、縦および斜めの消去は起こり得ないので、次の場所の処理に進む。

> ```
> 260 FORD=-1TO1
> ```

x座標の差分 `D` を変えながら、斜めおよび縦の判定を行う。

> ```
> IF[C-7-D]=[C]IF[C]=[C+7+D]E=1:FORE=-1TO1:LC13+X+D*E,3+Y+E:?CHR$(#F4);:NEXT
> ```

斜めまたは縦に3個同じブロックが並んでいる場合、消去フラグを立て、それらを消去する (画面に爆発パターンを書き込む)。
~~ループで変数 `E` を用いるため、その前の `E=1` は意味が無いはずだ。ミスだろう。~~

> ```
> 270 NEXT
> ```

x座標の差分 `D` のループを進める。

> ```
> 280 NEXT:NEXT
> ```

現在の座標 `X` および `Y` のループを進める。

> ```
> IFF*!EPLAY">F16."
> ```

これが今回初の消去判定であり (`F`)、かつ消去が行われなかった (`!E`) 場合、着地音を再生する。

> ```
> ELSEIFEPLAY"L16GB."
> ```

消去が行われた (`E`) 場合、消去音を再生する。

### 290～300行目、350行目：次のブロックの落下に進む

> ```
> 290 IFE+FWAIT30
> ```

消去が発生した場合、少し待ち、どこが消去されたかが見えるようにする。
消去が発生しなかった場合でも、初の消去判定である (すなわち、今回落下したブロックを配置したことによって消去されるブロックは全く無かった) 場合、次のブロックの落下開始前に待ちを入れる。

> ```
> 300 IF!EGOTO350
> ```

消去が完了した (消去するブロックが無かった) 場合、次のブロックの落下処理に進む。

> ```
> 350 Z=1679:LRUNFILE()-1
> ```

連携判定用の変数 `Z` を設定し、「描画・操作」のプログラムを実行する。
~~行をわけずに、300行目にこのまま書いてもよかったな。~~

### 310～340行目：消去対象のブロックの消去の実行

> ```
> 310 F=0
> ```

初判定フラグを折る。

> ```
> FORX=0TO5:P=13
> ```

各列についてブロックの消去・落下処理を行う。
ブロックの落下先 `P` を初期化する。

> ```
> FORY=12TO0STEP-1
> ```

下から上に向かって処理を行う。

> ```
> C=SCR(13+X,3+Y):IFC=#F4:B=B+1
> ```

画面のキャラクターをチェックする。
爆発パターンであれば、ブロックの消去を行い、消去カウント `B` を加算する。

> ```
> ELSEP=P-1:IFP>YLC13+X,3+P:?CHR$(C);:[9+7*P+X]=[9+7*Y+X]
> ```

爆発パターン (消去対象) でない場合は、ブロックの落下先を1段上げる。
そして、落下先が現在の位置より下である場合は、そこにブロックを移動する。

> ```
> 320 NEXT:IFPFORI=P-1TO0STEP-1:LC13+X,3+I:?CHR$(0);:[9+7*I+X]=0:NEXT
> ```

行に関するループを進める。
完了後、ブロックの上に空間が残っている場合は、空白で埋める。

> ```
> 330 NEXT:IFB>9999B=9999
> ```

列に関するループを進める。
完了後、消去カウントが 9999 を超える場合は、カンストさせる。

> ```
> 340 LC23,2:?DEC$(B,4);:WAIT30:GOTO230
> ```

消去カウントの表示を更新する。
少し待った後、(連鎖のため) 再び消去判定を実行する。