SmileBASIC（プチコン4）で作る、たぶん一番簡単なスネークゲーム

#はじめに
本当は別のテーマで書く予定でしたが、**コーディングが詰んでしまい**、急遽内容を変更しました...。

今回は、今年の夏前に**プチコン4**で作った**スネークゲーム**について書かせていただきました。

>
console.log()でスネークゲームを出力する
https://qiita.com/prononami/items/7571ce9bb382981a56bb

実はスネークゲームに関してはOrganizations内に先駆者がいますが、気にせず続けます。

#プチコン4
プチコン4公式サイト
https://www.petc4.smilebasic.com/

>
プチコン４は、Nintendo Switch上でプログラミングを楽しめるソフトウェアです。

というものです。価格は税込3,000円です。安い！
使用できる言語は「**SmileBASIC**」です。

#SmileBASIC
SmileBASICのリファレンスは[こちら](http://smilebasic.com/reference/)。

##特徴
- 型定義が不要、行末にセミコロン（；）が不要など、文法がシンプル。
- グラフィックや音声などの出力が楽
- オブジェクト指向ができない（それっぽいことはできるが制限あり）

正直言えば、Javaなどに慣れている自分からすると、少々書きにくかった印象があります。確かに初学者には易しいと思いますが。

#スネークゲーム実装
概要はこれくらいにして、実際にゲームを作っていきます。
まずは簡単に設計（のようなもの）から見ていきます。

##設計
以下、フローチャートです。
![SNAKE (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431212/39ac6bab-0b81-32c3-71dd-e9bdac9de1b8.jpeg)

また、今回は**コンソール出力**でゲームを表示します。~~本当にだだ被り...~~

##実装
###進む方向を決める
進む方向を決める**BECTOR関数**です。（例によって妙な命名）

```swift:BECTOR
// 方向と要素番号を対応
CONST #RIGHT = 0
CONST #UP = 1
CONST #LEFT = 2
CONST #DOWN = 3

// フレーム数
CONST #SPEED = 20
CONST #DASH = 4

// 蛇が向いている方向
DIM bector[2]
// 方向ごとに加算する値
DIM bectors[4, 2]
bectors[#RIGHT, 0] =  1 : bectors[#RIGHT, 1] =  0
bectors[#UP   , 0] =  0 : bectors[#UP   , 1] = -1
bectors[#LEFT , 0] = -1 : bectors[#LEFT , 1] =  0
bectors[#DOWN , 0] =  0 : bectors[#DOWN , 1] =  1

DEF BECTOR
  // 20フレーム経過したらループ終了
  FOR i = 0 TO #SPEED - 1
    IF BUTTON(CONTROLLER(1), #B_LRIGHT) == 1 THEN
      bector[0] = bectors[#RIGHT, 0]
      bector[1] = bectors[#RIGHT, 1]
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LUP) == 1 THEN
      bector[0] = bectors[#UP, 0]
      bector[1] = bectors[#UP, 1]
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LLEFT) == 1 THEN
      bector[0] = bectors[#LEFT, 0]
      bector[1] = bectors[#LEFT, 1]
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LDOWN) == 1 THEN
      bector[0] = bectors[#DOWN, 0]
      bector[1] = bectors[#DOWN, 1]
    ENDIF

    // Aボタンでダッシュ
    IF BUTTON(CONTROLLER(1), #B_RRIGHT) == 1 THEN
      // 4フレーム待つ
      VSYNC #DASH
      // ループ脱出
      BREAK
    ENDIF

    // 1フレーム待つ
    VSYNC 1
  NEXT
END
```

IFの中の判定ですが、

```swift
BUTTON(CONTROLLER(1), #B_LRIGHT) == 1
```

これは「Switch1Pの左コントローラーの右ボタン（いわゆる十字キー右）が押された状態か」を判定しています。
`#B_LRIGHT`は、そのボタンを表す定数で、プチコン4ではデフォルトで使用できます。また、SmileBASICでの真偽値は、TRUEとFALSEではなく**0と1**を使っています。（1が真）

BECTOR関数では、ループ1回で全方向のボタンの状態を確認し、1フレーム待ち、それを20フレーム分繰り返しています。

###進む先に何があるか判定

```swift:CHECK_NEXT
// ゲーム領域の限界値
CONST #BOX_LEFT = 1
CONST #BOX_RIGHT = 48
CONST #BOX_TOP = 1
CONST #BOX_BOTTOM = 28

// 蛇の体
CONST #SNAKE$ = "#"
// アイテム
CONST #NEW_HEAD$ = "ロ"

// 現在の頭の座標
DIM head[2]
// 次の頭の座標
DIM next_head[2]
// next_headにある文字のコード
DIM next_head_code

DEF CHECK_NEXT
  // 次の頭の位置を計算
  next_head[0] = head[0] + bector[0]
  next_head[1] = head[1] + bector[1]

  // 壁（ゲーム領域の限界）より外ならゲームオーバー
  IF next_head[0] < #BOX_LEFT OR next_head[0] > #BOX_RIGHT OR next_head[1] < #BOX_TOP OR next_head[1] > #BOX_BOTTOM THEN
    RETURN
  ENDIF

  next_head_code = CHKCHR(next_head[0], next_head[1])
  // アイテム（新しい体）の場合
  IF next_head_code == ASC(#NEW_HEAD$) THEN
    NEW_HEAD
  // 自分の体の場合
  ELSEIF next_head_code == ASC(#SNAKE$) THEN
    RETURN
  // 何もない場合
  ELSE WALK
  ENDIF
END
```

NEW_HEADやWALKは、後述する関数の名前です。

```swift
CHKCHR(x, y)
```
CHKCHRは、コンソール上の指定位置に表示されている文字のコードを返却する関数です。

```swift
ASC(char)
```
ASCは、引数に渡した文字のコードを返却する関数です。

```swift
next_head_code == ASC(#NEW_HEAD$)
```

次の頭の位置にある文字が何かを、コードで判定しています。
該当した文字によって、次の処理を分岐させています。

###歩く
```swift:WALK
// 最後尾の座標
DIM tail[2]
// 頭の座標
DIM head[2]

DEF WALK
  // 最後尾の位置の体（#）を消去
  LOCATE tail[0], tail[1]
  ? " "

  SET_BODY next_head

  次の頭の位置に体（#）を表示
  LOCATE head[0], head[1]
  ? #SNAKE$
  
  // BECTOR関数へ移行
  BECTOR
END
```

```swift:SET_BODY
// 蛇の体全体を表す二次元配列
// 体の位置の座標を格納
DIM body[1500,2]
DIM head[2]
DIM tail[2]
// 蛇の体の長さ
DIM snake_cnt

// 引数：next_head[2]（次の頭の位置）
DEF SET_BODY next_haed
  // 体の位置を1つずつズラす
  FOR i = 0 TO snake_cnt - 2
    body[i, 0] = body[i + 1, 0]
    body[i, 1] = body[i + 1, 1]
  NEXT
  
  // 頭の位置に引数の値を格納
  body[snake_cnt - 1, 0] = next_head[0]
  body[snake_cnt - 1, 1] = next_head[1]
  SET_HEAD
  SET_TAIL
END
```

```swift:SET_HEAD
// 頭の位置を保持
DEF SET_HEAD
  head[0] = body[snake_cnt - 1, 0]
  head[1] = body[snake_cnt - 1, 1]
END
```

```swift:SET_TAIL
// 最後尾の位置を保持
DEF SET_TAIL
  tail[0] = body[0, 0]
  tail[1] = body[0, 1]
END
```

図でいうとこんな感じです。
![SNAKE_WALK.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431212/2cd1b0be-9823-5ae4-4aef-5af50974c256.jpeg)

「体のためにわざわざ配列使わなくてもいいかな」とも思いましたが、消去する尻尾の判定ができなくなる（できても計算量が多くなる）ので、このようにしました。

###新しい体を追加する

```swift:NEW_HEAD
// 点数
DIM score

DEF NEW_HEAD
  // scoreを1増やす
  // 「score++」と同じ
  INC score

  LOCATE 7, 0
  ? score

  // 体を増やす処理へ移行
  ADD_BODY next_head

  LOCATE next_head[0], next_head[1]
  ? #SNAKE$

  // 次のアイテムの出現位置を決める
  LOOP
    new_head[0] = RND(48) + 1
    new_head[1] = RND(48) + 1
    chr = CHKCHR(new_head[0], new_head[1])
    // 蛇の体がある位置には出現させない
    IF chr != ASC(#SNAKE$) THEN BREAK
  ENDLOOP

  LOCATE new_head[0], new_head[1]
  ? #NEW_HEAD$

  // BECTOR関数へ移行
  BECTOR
END
```

```swift:ADD_BODY
// 新しい体を追加する
// 引数：新しい体の位置
DEF ADD_BODY new_head
  body[snake_cnt, 0] = new_head[0]
  body[snake_cnt, 1] = new_head[1]
  INC snake_cnt
  SET_HEAD
END
```

頭の位置は変わりますが、尻尾の位置は変わらないので、SET_HEADのみを実行します。

体の配列で、0番目を頭ではなく尻尾にした理由は、体を追加する処理をラクにするためです。~~（結果論）~~

##コード全体
以上の処理を整理し、多少の演出を加え、完成したコードがこちらです。（長いので折りたたんでます）

<details><summary>ソースコード全文</summary>
<div>

```swift:MAIN.PRG
// 蛇の初期位置
CONST #CENTER_X = 25
CONST #CENTER_Y = 15
// ゲーム領域の限界値
CONST #BOX_LEFT = 1
CONST #BOX_RIGHT = 48
CONST #BOX_TOP = 1
CONST #BOX_BOTTOM = 28
// オブジェクトで使う文字
CONST #SNAKE$ = "#"
CONST #NEW_HEAD$ = "ロ"
CONST #MISS$ = "※"
// 進む方向を表す定数
CONST #RIGHT = 0
CONST #UP = 1
CONST #LEFT = 2
CONST #DOWN = 3
// フレーム数
CONST #SPEED = 20
CONST #DASH = 4

DIM body[1500,2]
DIM head[2]
DIM tail[2]
DIM snake_cnt
DIM new_head[2]

DIM bector[2]
DIM bectors[4, 2]
bectors[#RIGHT, 0] =  1 : bectors[#RIGHT, 1] =  0
bectors[#UP   , 0] =  0 : bectors[#UP   , 1] = -1
bectors[#LEFT , 0] = -1 : bectors[#LEFT , 1] =  0
bectors[#DOWN , 0] =  0 : bectors[#DOWN , 1] =  1

DIM next_head[2]
DIM next_head_code

DIM score

DEF SET_HEAD
  head[0] = body[snake_cnt - 1, 0]
  head[1] = body[snake_cnt - 1, 1]
END

DEF SET_TAIL
  tail[0] = body[0, 0]
  tail[1] = body[0, 1]
END

DEF SET_BODY next_haed
  FOR i = 0 TO snake_cnt - 2
    body[i, 0] = body[i + 1, 0]
    body[i, 1] = body[i + 1, 1]
  NEXT
  body[snake_cnt - 1, 0] = next_head[0]
  body[snake_cnt - 1, 1] = next_head[1]
  SET_HEAD
  SET_TAIL
END

DEF ADD_BODY new_head
  body[snake_cnt, 0] = new_head[0]
  body[snake_cnt, 1] = new_head[1]
  INC snake_cnt
  SET_HEAD
END

// 蛇の初期化
DEF RESET_BODY
  body[0, 0] = #CENTER_X
  body[0, 1] = #CENTER_Y
  snake_cnt = 1
  score = 0
  SET_HEAD
  SET_TAIL
END

// ゲーム画面作成
DEF STAGE_CREATE
  // Clean Screen
  CLS
  // 長方形を表示する
  GBOX 7, 7, 392, 232, #C_WHITE
  COLOR #C_WHITE
  LOCATE 1, 0
  ? "SCORE:0"

  LOOP
    new_head[0] = RND(48) + 1
    new_head[1] = RND(28) + 1
    IF new_head[0] != #CENTER_X OR new_head[1] != #CENTER_Y THEN BREAK
  END LOOP

  LOCATE #CENTER_X, #CENTER_Y
  ? #SNAKE$
END

// ゲーム開始
DEF GAME_START
  // BGMを流す
  BGMPLAY 27

  // 最初はボタンが押されるまで動かない
  bector[0] = 0
  bector[1] = 0
  LOOP
    IF BUTTON(CONTROLLER(1), #B_LRIGHT) == 1 THEN
      bector[0] = bectors[#RIGHT, 0]
      bector[1] = bectors[#RIGHT, 1]
      BREAK
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LUP) == 1 THEN
      bector[0] = bectors[#UP, 0]
      bector[1] = bectors[#UP, 1]
      BREAK
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LLEFT) == 1 THEN
      bector[0] = bectors[#LEFT, 0]
      bector[1] = bectors[#LEFT, 1]
      BREAK
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LDOWN) == 1 THEN
      bector[0] = bectors[#DOWN, 0]
      bector[1] = bectors[#DOWN, 1]
      BREAK
    ENDIF
  ENDLOOP

  // 次の頭の位置のチェックへ
  CHECK_NEXT
END

DEF CHECK_NEXT
  next_head[0] = head[0] + bector[0]
  next_head[1] = head[1] + bector[1]

  IF next_head[0] < #BOX_LEFT OR next_head[0] > #BOX_RIGHT OR next_head[1] < #BOX_TOP OR next_head[1] > #BOX_BOTTOM THEN
    RETURN
  ENDIF

  next_head_code = CHKCHR(next_head[0], next_head[1])
  IF next_head_code == ASC(#NEW_HEAD$) THEN
    NEW_HEAD
  ELSEIF next_head_code == ASC(#SNAKE$) THEN
    RETURN
  ELSE WALK
  ENDIF
END

DEF BECTOR
  FOR i = 0 TO #SPEED - 1
    IF BUTTON(CONTROLLER(1), #B_LRIGHT) == 1 THEN
      bector[0] = bectors[#RIGHT, 0]
      bector[1] = bectors[#RIGHT, 1]
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LUP) == 1 THEN
      bector[0] = bectors[#UP, 0]
      bector[1] = bectors[#UP, 1]
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LLEFT) == 1 THEN
      bector[0] = bectors[#LEFT, 0]
      bector[1] = bectors[#LEFT, 1]
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_LDOWN) == 1 THEN
      bector[0] = bectors[#DOWN, 0]
      bector[1] = bectors[#DOWN, 1]
    ENDIF

    IF BUTTON(CONTROLLER(1), #B_RRIGHT) == 1 THEN
      VSYNC #DASH
      BREAK
    ENDIF

    VSYNC 1
  NEXT

  CHECK_NEXT
END

DEF WALK
  LOCATE tail[0], tail[1]
  ? " "

  SET_BODY next_head

  LOCATE head[0], head[1]
  ? #SNAKE$

  BEEP 59, 0, 16

  BECTOR
END

DEF NEW_HEAD
  INC score

  LOCATE 7, 0
  ? score

  ADD_BODY next_head

  LOCATE next_head[0], next_head[1]
  ? #SNAKE$

  BEEP 12

  LOOP
    new_head[0] = RND(48) + 1
    new_head[1] = RND(48) + 1
    cnt = CHKCHR(new_head[0], new_head[1])
    IF cnt != ASC(#SNAKE$) THEN BREAK
  ENDLOOP

  LOCATE new_head[0], new_head[1]
  ? #NEW_HEAD$

  BECTOR
END

DEF MISS
  LOCATE head[0], head[1]
  ? #MISS$
  BGMSTOP
  BEEP 13
  VSYNC 150
END

// メイン処理
STAGE_CREATE
GAME_START
MISS
```

</div>
</details>

実際に処理が始まるのは、最後の方にある「STAGE_CREATE」からです。あまりに簡略化されて分かりにくいですが、イメージはこんな感じです。
![SNAKE_FINALLY.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431212/1cde79fb-f359-cdaa-2857-d7ec994d2e49.jpeg)

実際のゲーム画面です

↓アイテムに向かう
![SNAKE_Qiita_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431212/c5b0ce9c-2c56-15fe-f63b-02420a4b2228.jpeg)

↓アイテムを取ると、蛇の体とスコアが増え、新たなアイテムが出現
![SNAKE_Qiita_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431212/fa4ce02b-e8f1-760a-5109-dfb6ab9f49de.jpeg)

↓無駄に200くらいまで頑張ってみた
![SNAKE_Qiita_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431212/3fc47ccd-1cd5-599f-b3e8-78c7f4cb5697.jpeg)

#総括

- 過去に作ったコードを見直すと、結構粗が見えた。自分の実力が上がったということにしたい
- 方向を決める部分で、実は若干操作性が悪い。フレーム関連と仲良くしないといけないかもしれない。
- 記事用にコードを手打ちで書き写すのが一番大変でした。

今回書く予定だったけど断念したものに関しては....なんとかします....。

#オマケ
このスネークゲーム、アプデにアプデを重ね、今はこんなことになっています。
![SNAKE_Qiita_4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431212/c93f2c79-08c1-3bd7-61fc-eb45463092a8.jpeg)

実装したもの

- 障害物
- 障害物を壊せるアイテム
- 残機
- 強制方向転換
- ワープ
- 首尾逆転
- 強制ダッシュ

何のゲームだっけな...。