【IchigoJam】学ランか セーラー服か ルーレット【川柳】

# はじめに

みなさんは、学ランを着ようかセーラー服を着ようか迷ったことはあるだろうか？
きっとある人も、ない人も居るだろう。
そんな状況でも、それ以外の状況でも、学ランかセーラー服かをランダムに決めてくれるルーレットを [IchigoJam](https://ichigojam.net/) で作ってみた。

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

# 画像の圧縮

今回は、64×48ピクセルの画像データを2枚用意し、画面上に交互に表示したい。
1ピクセルを1ビットで表すとこれは1枚あたり3072ビットとなり、1文字で6ビットを表すと512文字になる。
これでは、2枚で1024文字となり、これだけで IchigoJam のプログラム領域を埋め尽くしてしまう。
そこで、画像データを圧縮して格納することにした。

今回用意した画像データは背景色の部分が多いため、背景色のみをランレングス圧縮すればよい。
これを踏まえて、今回は以下のような形式のエンコードを採用した。

* 画面上の1文字 (田の形の4ピクセル) を4ビットで表す
* 背景色でないピクセルがある文字は、3文字 (12ビット) を2文字にエンコードする
  * 文字コードの下位6ビットを用いて、1文字目の上位ビットから2文字目の下位ビットに向かって4ビット×3を格納する
  * 背景色のみのピクセルの2個以下の連続も、この形式で表す
* 背景色のピクセルのみの文字が3個以上連続する場合は、連続する個数をエンコードする
  * 15個以下の場合は、連続する個数に `#20` を足した文字コードの文字で表す
  * 16個以上94個以下の場合は、`!` に続いて連続する個数に `#20` を足した文字コードの文字を置いて表す

以下の Python プログラムは、画像ファイルとエンコードする部分を受け取り、この形式でエンコードした文字列を出力する。

<details><summary>img_assyuku.py</summary>

```python:img_assyuku.py
# coding: utf-8

import sys
from PIL import Image

if len(sys.argv) != 2 and len(sys.argv) != 6:
	sys.stderr.write("Usage: img_assyuku.py input_file [sx sy width height]\n")
	sys.exit(1)

img = Image.open(sys.argv[1]).convert("L")
(img_width, img_height) = img.size

if len(sys.argv) == 6:
	sx = int(sys.argv[2])
	sy = int(sys.argv[3])
	width = int(sys.argv[4])
	height = int(sys.argv[5])
else:
	sx = 0
	sy = 0
	width = img_width
	height = img_height

if width % 2 != 0 or height % 2 != 0:
	sys.stderr.write("please use even width and height")
	sys.exit(1)

if sx < 0 or sy < 0 or sx + width > img_width or sy + height > img_height:
	sys.stderr.write("cropping rectangle out of range")
	sys.exit(1)

data = []

for y in range(sy, sy + height, 2):
	for x in range(sx, sx + width, 2):
		value = 0
		if img.getpixel((x, y)) <= 128: value += 1
		if img.getpixel((x + 1, y)) <= 128: value += 2
		if img.getpixel((x, y + 1)) <= 128: value += 4
		if img.getpixel((x + 1, y + 1)) <= 128: value += 8
		data.append(value)

char_table = "".join(map(chr, range(0x40, 0x70))) + "".join(map(chr, range(0x30, 0x40)))

result = ""
i = 0
data_len = len(data)
while i < data_len:
	if i + 3 > data_len:
		sys.stderr.write("Oops! this image is not supported in this version!\n")
		sys.stderr.write("partial result:\n")
		sys.stderr.write(result)
		sys.stderr.write("\n")
		sys.exit(1)
	if sum(data[i:i+3]) == 0:
		zero_end = i + 3
		while zero_end < data_len:
			if data[zero_end] != 0: break
			zero_end += 1
		zero_combo = zero_end - i
		if zero_combo > 0x5e: zero_combo = 0x5e
		if zero_combo > 0xf: result += "!"
		result += chr(zero_combo + 0x20)
		i += zero_combo
	else:
		result += char_table[(data[i] << 2) | (data[i + 1] >> 2)]
		result += char_table[((data[i + 1] & 3) << 4) | data[i + 2]]
		i += 3

print(result)
```

</details>

# 画像の展開

上で用意したプログラムで画像ファイルをエンコードした文字列を埋め込み、さらにデコーダを実装した。
デコードは、以下のようにエンコードの逆を行い、VRAMにグラフィック文字を書き込む。

* 文字 `!` があったら、その次の文字の文字コードから `#20` を引いた個数の背景色 (文字コード `#80`) を書き込む
* それ以外で文字コード `#30` 未満の文字があったら、その文字の文字コードから `#20` を引いた個数の背景色 (文字コード `#80`) を書き込む
* それ以外の文字があったら、その次の文字とのペアから4ビットの数値3個を取り出し、対応するグラフィック文字を書き込む

```text
10 ' ｶﾞｸﾗﾝ ｾｰﾗｰﾌｸ ﾃﾞﾓ
20 G="!G3D!<i@8@!9cbNl!8`1%NP!5h@@A#h@!4hD@D@Dh@@DQ@cDA@(hE%V`@Jo;CL0BB@&hE@A@Eh@BL;@@F#X@%hE@D@Eh@@HT@a`CF(hE%V`!5hE@A@Eh@!4kE%7`!77L3LT@!8T@AP!:T@AP!:TEAP!:TEAP!:TEAP!:TEAP!:TEAP!:TEAP!:L3LP!T"
30 S="!~'acL<!8`5I@XGd@!4hBRXE`h@!4hDKMXDh@B@@H0@CL0@%hEBM@Eh@Bk`H2@CUH@%hEBo@Eh@Bb@0hCC_l@%hE@A@Eh@@c@BD@b]d@%hM3L3Eh@!4kEhEhM8@!4kEhEhM8@!6V`V`T@!6`ZAZBP!7hJAZB`!7hJAZB`!7H3L3L0!~!~!7"
40 C=0
50 A=#900:IF C P=S ELSE P=G
60 D=PEEK(P):IFD=#22GOTO100
70 IF D=#21:E=PEEK(P+1):FORI=#21TOE:POKEA,#80:A=A+1:NEXT:P=P+2:GOTO60
80 IF D<#30:FORI=#21TOD:POKEA,#80:A=A+1:NEXT:P=P+1:GOTO60
90 E=PEEK(P+1):POKEA,#80+D>>2&#F,D&3<<2|E>>4&3+#80,#80+E&#F:A=A+3:P=P+2:GOTO60
100 IF !INKEY() GOTO100
110 C=!C:GOTO50
```

IchigoJam Q (IchigoJam BASIC 1.4.3) で実行すると、1枚デコードするのに3～4秒程度かかった。

![BASIC 版デコーダの実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/df35c575-aa3e-48de-35fb-1c4d8d95a9d6.gif)

なお、画像中の「学ラン」「セーラー服」の文字は[美咲ゴシック](https://littlelimit.net/misaki.htm)を使用している。

# 画像の展開をマシン語に移植する

BASIC での実装によりデコーダのアルゴリズムは良さそうであることが確認できたが、このままでは遅すぎてルーレットとして使い物にならなそうである。
そこで、このアルゴリズムをマシン語に移植した。

<details><summary>アセンブリコード</summary>

```text
' R0 : 文字のアドレス
' R1 : 画面のアドレス
	R0 += R1
	R2 = 9
	R2 = R2 << 8
	R1 += R2
@MAINLOOP
	R2 = [R0]
	R2 - #22
	IF 0 GOTO @END
	R2 - #30
	IF CS GOTO @DIRECT
	R2 - #21
	IF !0 GOTO @ONEREPEAT
	R0 += 1
	R2 = [R0]
@ONEREPEAT
	R0 += 1
	R2 -= #20
	R3 = #80
@REPEATLOOP
	[R1] = R3
	R1 += 1
	R2 -= 1
	IF !0 GOTO @REPEATLOOP
	GOTO @MAINLOOP
@DIRECT
	R2 = R2 << 26
	R3 = [R0 + 1]
	R3 = R3 << 26
	R3 = R3 >> 6
	R2 |= R3
	R3 = R2 >> 28
	R3 += #80
	[R1] = R3
	R2 = R2 << 4
	R3 = R2 >> 28
	R3 += #80
	[R1 + 1] = R3
	R2 = R2 << 4
	R3 = R2 >> 28
	R3 += #80
	[R1 + 2] = R3
	R0 += 2
	R1 += 3
	GOTO @MAINLOOP
@END
	RET
```

</details>

```text
10 ' ｶﾞｸﾗﾝ ｾｰﾗｰﾌｸ ﾃﾞﾓ 2
20 G="!G3D!<i@8@!9cbNl!8`1%NP!5h@@A#h@!4hD@D@Dh@@DQ@cDA@(hE%V`@Jo;CL0BB@&hE@A@Eh@BL;@@F#X@%hE@D@Eh@@HT@a`CF(hE%V`!5hE@A@Eh@!4kE%7`!77L3LT@!8T@AP!:T@AP!:TEAP!:TEAP!:TEAP!:TEAP!:TEAP!:TEAP!:L3LP!T"
30 S="!~'acL<!8`5I@XGd@!4hBRXE`h@!4hDKMXDh@B@@H0@CL0@%hEBM@Eh@Bk`H2@CUH@%hEBo@Eh@Bb@0hCC_l@%hE@A@Eh@@c@BD@b]d@%hM3L3Eh@!4kEhEhM8@!4kEhEhM8@!6V`V`T@!6`ZAZBP!7hJAZB`!7hJAZB`!7H3L3L0!~!~!7"
40 LET[0],17416,8713,530,17425,30722,10786,#D020,10800,#D20B,10785,#D101,12289,30722,12289,14880,9088,28683,12545,14849,#D1FB,-6162,1682,30787,1691,2459,17178,3859,13184,28683,274,3859,13184,28747
50 LET[33],274,3859,13184,28811,12290,12547,-6181,18288
60 C=0
70 IF C P=S ELSE P=G
80 X=USR(#800,P)
90 IF !INKEY() GOTO90
100 C=!C:GOTO70
```

IchigoJam Q (IchigoJam BASIC 1.4.3) で実行すると、一瞬で画像を切り替えることができた。
ただし、このプログラムは IchigoJam R には非対応である。

# ルーレットの機能を実装する

画像を素早く切り替えることができたので、あとはルーレットの機能を実装すれば完成である。
というわけで、実装した。

```text
10 ' ｶﾞｸﾗﾝ or ｾｰﾗｰﾌｸ ﾙｰﾚｯﾄ
20 G="!G3D!<i@8@!9cbNl!8`1%NP!5h@@A#h@!4hD@D@Dh@@DQ@cDA@(hE%V`@Jo;CL0BB@&hE@A@Eh@BL;@@F#X@%hE@D@Eh@@HT@a`CF(hE%V`!5hE@A@Eh@!4kE%7`!77L3LT@!8T@AP!:T@AP!:TEAP!:TEAP!:TEAP!:TEAP!:TEAP!:TEAP!:L3LP!T"
30 S="!~'acL<!8`5I@XGd@!4hBRXE`h@!4hDKMXDh@B@@H0@CL0@%hEBM@Eh@Bk`H2@CUH@%hEBo@Eh@Bb@0hCC_l@%hE@A@Eh@@c@BD@b]d@%hM3L3Eh@!4kEhEhM8@!4kEhEhM8@!6V`V`T@!6`ZAZBP!7hJAZB`!7hJAZB`!7H3L3L0!~!~!7"
40 LET[0],17416,8713,530,17425,30722,10786,#D020,10800,#D20B,10785,#D101,12289,30722,12289,14880,9088,28683,12545,14849,#D1FB,-6162,1682,30787,1691,2459,17178,3859,13184,28683,274,3859,13184,28747
50 LET[33],274,3859,13184,28811,12290,12547,-6181,18288
60 C=1:D=0:E=0:F=-1
70 C=!C:IF C P=S ELSE P=G
80 X=USR(#800,P)
90 IF D>0 BEEP:D=D-1:WAIT5:GOTO70
100 IF E>0 BEEP:E=E-1:WAIT10:GOTO70
110 IF F>0 BEEP:F=F-1:WAIT20:GOTO70
120 IF F=0 PLAY"T160L16O5CDEFG"
130 IF BTN() GOTO130
140 IF !INKEY() AND !BTN() GOTO140
150 D=RND(13)+36:E=RND(7)+6:F=RND(4)+3:GOTO70
```

* [OneFiveCrowd で実行する](https://onefivecrowd.mikecat.work/#text=RUN%0A&prg=CgAYJyC23rjX3SBvciC-sNewzLgg2bDar8QAABQAwEc9IiFHM0QhPGlAOEAhOWNiTmwhOGAxJU5QITVoQEBBI2hAITRoREBEQERoQEBEUUBjREFAKGhFJVZgQEpvO0NMMEJCQCZoRUBBQEVoQEJMO0BARiNYQCVoRUBEQEVoQEBIVEBhYENGKGhFJVZgITVoRUBBQEVoQCE0a0UlN2AhNzdMM0xUQCE4VEBBUCE6VEBBUCE6VEVBUCE6VEVBUCE6VEVBUCE6VEVBUCE6VEVBUCE6VEVBUCE6TDNMUCFUIgAeALhTPSIhfidhY0w8IThgNUlAWEdkQCE0aEJSWEVgaEAhNGhES01YRGhAQkBASDBAQ0wwQCVoRUJNQEVoQEJrYEgyQENVSEAlaEVCb0BFaEBCYkAwaENDX2xAJWhFQEFARWhAQGNAQkRAYl1kQCVoTTNMM0VoQCE0a0VoRWhNOEAhNGtFaEVoTThAITZWYFZgVEAhNmBaQVpCUCE3aEpBWkJgITdoSkFaQmAhN0gzTDNMMCF-IX4hNyIAACgAwkxFVFswXSwxNzQxNiw4NzEzLDUzMCwxNzQyNSwzMDcyMiwxMDc4NiwjRDAyMCwxMDgwMCwjRDIwQiwxMDc4NSwjRDEwMSwxMjI4OSwzMDcyMiwxMjI4OSwxNDg4MCw5MDg4LDI4NjgzLDEyNTQ1LDE0ODQ5LCNEMUZCLC02MTYyLDE2ODIsMzA3ODcsMTY5MSwyNDU5LDE3MTc4LDM4NTksMTMxODQsMjg2ODMsMjc0LDM4NTksMTMxODQsMjg3NDcAADIANExFVFszM10sMjc0LDM4NTksMTMxODQsMjg4MTEsMTIyOTAsMTI1NDcsLTYxODEsMTgyODgAPAAQQz0xOkQ9MDpFPTA6Rj0tMQBGABZDPSFDOklGIEMgUD1TIEVMU0UgUD1HAFAADlg9VVNSKCM4MDAsUCkAAFoAHklGIEQ-MCBCRUVQOkQ9RC0xOldBSVQ1OkdPVE83MABkACBJRiBFPjAgQkVFUDpFPUUtMTpXQUlUMTA6R09UTzcwAABuACBJRiBGPjAgQkVFUDpGPUYtMTpXQUlUMjA6R09UTzcwAAB4ABxJRiBGPTAgUExBWSJUMTYwTDE2TzVDREVGRyIAAIIAEElGIEJUTigpIEdPVE8xMzAAjAAeSUYgIUlOS0VZKCkgQU5EICFCVE4oKSBHT1RPMTQwAJYAKkQ9Uk5EKDEzKSszNjpFPVJORCg3KSs2OkY9Uk5EKDQpKzM6R09UTzcw) (高速、非公式) (推奨)
* [IchigoJam web で実行する](https://fukuno.jig.jp/app/IchigoJam/#10%20%27%20%EF%BD%B6%EF%BE%9E%EF%BD%B8%EF%BE%97%EF%BE%9D%20or%20%EF%BD%BE%EF%BD%B0%EF%BE%97%EF%BD%B0%EF%BE%8C%EF%BD%B8%20%EF%BE%99%EF%BD%B0%EF%BE%9A%EF%BD%AF%EF%BE%84%0A20%20G%3D%22!G3D!%3Ci%408%40!9cbNl!8%601%25NP!5h%40%40A%23h%40!4hD%40D%40Dh%40%40DQ%40cDA%40%28hE%25V%60%40Jo%3BCL0BB%40%26hE%40A%40Eh%40BL%3B%40%40F%23X%40%25hE%40D%40Eh%40%40HT%40a%60CF%28hE%25V%60!5hE%40A%40Eh%40!4kE%257%60!77L3LT%40!8T%40AP!%3AT%40AP!%3ATEAP!%3ATEAP!%3ATEAP!%3ATEAP!%3ATEAP!%3ATEAP!%3AL3LP!T%22%0A30%20S%3D%22!~%27acL%3C!8%605I%40XGd%40!4hBRXE%60h%40!4hDKMXDh%40B%40%40H0%40CL0%40%25hEBM%40Eh%40Bk%60H2%40CUH%40%25hEBo%40Eh%40Bb%400hCC_l%40%25hE%40A%40Eh%40%40c%40BD%40b%5Dd%40%25hM3L3Eh%40!4kEhEhM8%40!4kEhEhM8%40!6V%60V%60T%40!6%60ZAZBP!7hJAZB%60!7hJAZB%60!7H3L3L0!~!~!7%22%0A40%20LET%5B0%5D%2C17416%2C8713%2C530%2C17425%2C30722%2C10786%2C%23D020%2C10800%2C%23D20B%2C10785%2C%23D101%2C12289%2C30722%2C12289%2C14880%2C9088%2C28683%2C12545%2C14849%2C%23D1FB%2C-6162%2C1682%2C30787%2C1691%2C2459%2C17178%2C3859%2C13184%2C28683%2C274%2C3859%2C13184%2C28747%0A50%20LET%5B33%5D%2C274%2C3859%2C13184%2C28811%2C12290%2C12547%2C-6181%2C18288%0A60%20C%3D1%3AD%3D0%3AE%3D0%3AF%3D-1%0A70%20C%3D!C%3AIF%20C%20P%3DS%20ELSE%20P%3DG%0A80%20X%3DUSR%28%23800%2CP%29%0A90%20IF%20D%3E0%20BEEP%3AD%3DD-1%3AWAIT5%3AGOTO70%0A100%20IF%20E%3E0%20BEEP%3AE%3DE-1%3AWAIT10%3AGOTO70%0A110%20IF%20F%3E0%20BEEP%3AF%3DF-1%3AWAIT20%3AGOTO70%0A120%20IF%20F%3D0%20PLAY%22T160L16O5CDEFG%22%0A130%20IF%20BTN%28%29%20GOTO130%0A140%20IF%20!INKEY%28%29%20AND%20!BTN%28%29%20GOTO140%0A150%20D%3DRND%2813%29%2B36%3AE%3DRND%287%29%2B6%3AF%3DRND%284%29%2B3%3AGOTO70%0ARUN%0A) (公式、低速)

# まとめ

画像を独自の形式でエンコードして文字列としてプログラムに埋め込み、それをマシン語で素早くデコードして表示することで、2種類の画像の中からランダムで1種類を選ぶルーレットを作成できた。