ORANGE pico で「セパクイズ」を作ってみた。
これは、日本のプロ野球の球団をセ・リーグとパ・リーグに分類するクイズである。
ソースコードからすぐに答えがバレないよう、問題データをArcfourで暗号化して格納している。
プログラム
ソースコード
10 dim enc0(63) = [139,184,129,3,110,106,103,244,209,155,0,208,174,232,3,202,233,73,219,236,2,250,168,130,144,235,2,70,96,97,177,134,108,39,149,132,85,216,192,156,205,126,135,11,122,227,216,211,195,65,73,203,197,55,25,66,215,42,43,238,169,106,193,13]
20 dim enc1(63) = [75,39,192,108,105,161,210,85,54,163,123,209,184,173,174,78,113,243,104,6,157,187,168,37,183,24,127,110,173,140,209,94,78,100,57,46,39,68,97,253,254,249,240,76,135,236,210,163,145,204,227,38,53,147,229,164,193,49,168,145,70,189,186,137]
30 dim enc2(63) = [145,240,173,136,26,125,28,189,0,247,219,171,235,138,131,195,160,153,175,205,116,127,168,250,30,239,226,182,69,178,72,62,197,59,204,95,5,225,244,185,242,210,54,25,42,138,6,182,219,236,222,80,175,212,62,152,94,198,196,66,49,196,45,210]
40 dim enc3(63) = [144,120,230,44,99,137,133,130,80,73,61,143,55,195,122,99,35,143,54,210,222,174,176,44,68,88,81,167,51,163,35,172,236,177,2,55,253,126,223,228,122,175,143,216,174,10,166,242,70,49,2,0,113,61,100,43,18,179,163,53]
50 dim teams(11,15)
60 teams(0,0) = 0 : tcnt = 0 : first = 0
70 password$ = "baseball"
80 dim s(255)
90 for i = 0 to 255
100 s(i) = i
110 next
120 j = 0
130 for i = 0 to 255
140 c$ = mid$(password$,i % len(password$) + 1,1) : j = (j + s(i) + asc(c$)) % 256
150 t = s(i) : s(i) = s(j) : s(j) = t
160 next
170 i = 0 : j = 0 : l = 0
180 i = (i + 1) % 256
190 j = (j + s(i)) % 256
200 t = s(i) : s(i) = s(j) : s(j) = t
210 toxor = s((s(i) + s(j)) % 256)
220 if l < 64 * 1 then c = enc0(l % 64) : goto 260
230 if l < 64 * 2 then c = enc1(l % 64) : goto 260
240 if l < 64 * 3 then c = enc2(l % 64) : goto 260
250 c = enc3(l % 64) : goto 260
260 c = c ^ toxor : l = l + 1
270 if c == 10 then goto 300
280 if first > 0 || !((&H81 <= c && c <= &H9F) || (&HE0 <= c && c <= &HFC)) then teams(tcnt,0) = teams(tcnt,0) + 1 : teams(tcnt,teams(tcnt,0)) = first * 256 + c : first = 0 else first = c
290 goto 180
300 tcnt = tcnt + 1
310 if tcnt >= 12 then goto 340
320 teams(tcnt,0) = 0
330 goto 180
340 dim title(4)=[&H835A,&H8370,&H834E,&H8343,&H8359]
350 dim question(22)=[&H8E9F,&H82CC,&H8B85,&H9263,&H82F0,&H8141,&H835A,&H8145,&H838A,&H815B,&H834F,&H82C6,&H8370,&H8145,&H838A,&H815B,&H834F,&H82C9,&H95AA,&H97DE,&H82B9,&H82E6,&H8142]
360 dim group0(4)=[&H835A,&H8145,&H838A,&H815B,&H834F]
370 dim group1(4)=[&H8370,&H8145,&H838A,&H815B,&H834F]
380 dim controll0(8)=[&H8FE3,&H89BA,&H8DB6,&H8945,&H834C,&H815B,&H82C5,&H9149,&H91F0]:dim controll1(10)=[&H8141,&H45,&H6E,&H74,&H65,&H72,&H834C,&H815B,&H82C5,&H92F1,&H8F6F]
390 dim teamx(2)=[56,160,264]
400 dim shuffle(11):dim selection(11)
410 black=rgb(0,0,0):white=rgb(255,255,255):upkey=30:downkey=31:leftkey=28:rightkey=29:enterkey=13:titlex=140:titley=0:questionx=68:questiony=20:groupy=40:targety=60:controllx=76:controlly=190:arrowoffset=64
420 cls:selected=0:cansubmit=0:initializing=1
430 for i=0 to 4
440 cpeek title(i)& &HFFFF:gput titlex+8*i,titley,8,8000,white
450 cpeek group0(i)& &HFFFF:gput teamx(0)-20+8*i,groupy,8,8000,white
460 cpeek group1(i)& &HFFFF:gput teamx(2)-20+8*i,groupy,8,8000,white
470 next
480 for i=0 to 22
490 cpeek question(i)& &HFFFF:gput questionx+8*i,questiony,8,8000,white
500 next
510 for i=0 to 8
520 cpeek controll0(i)& &HFFFF:gput controllx+8*i,controlly,8,8000,white
530 next
540 for i=0 to 11
550 shuffle(i)=i:selection(i)=1
560 next
570 for i=0 to 11
580 pos=i+rnd(12-i):temp=shuffle(i):shuffle(i)=shuffle(pos):shuffle(pos)=temp
590 next
600 for i=0 to 11
610 teamid=shuffle(i):thistargety=targety+10*i
620 for j=0 to 7
630 line 0,targety+10*i+j,320,thistargety+j,black
640 next
650 for j=0 to teams(teamid,0)-1
660 targetx=teamx(selection(i))-4*teams(teamid,0)
670 cpeek teams(teamid,j+1)& &HFFFF:gput targetx+8*j,thistargety,8,8000,white
680 if i<>selected then goto 710
690 if selection(i)>0 then cpeek -1,84:gput teamx(selection(i))-arrowoffset-8,thistargety,8,8000,white
700 if selection(i)<2 then cpeek -1,85:gput teamx(selection(i))+arrowoffset,thistargety,8,8000,white
710 next
720 if initializing==0 then return
730 next
740 initializing=0
750 k=inkey()
760 curselected=selected:curcansubmit=cansubmit:curselection=selection(selected)
770 if k==leftkey && selection(selected)>0 then selection(selected)=selection(selected)-1
780 if k==rightkey && selection(selected)<2 then selection(selected)=selection(selected)+1
790 if k==upkey then selected=(selected+11)%12
800 if k==downkey then selected=(selected+1)%12
810 if k==enterkey && cansubmit then goto 980
820 if curselected<>selected then i=curselected:gosub 610
830 if curselected<>selected || curselection<>selection(selected) then i=selected:gosub 610
840 cansubmit=1
850 for i=0 to 11
860 if selection(i)==1 then cansubmit=0
870 next
880 if curcansubmit==cansubmit then goto 750
890 if cansubmit then goto 940
900 for i=0 to 7
910 line controllx+8*9,controlly+i,320,controlly+i,black
920 next
930 goto 750
940 for i=0 to 10
950 cpeek controll1(i)& &HFFFF:gput controllx+8*9+8*i,controlly,8,8000,white
960 next
970 goto 750
980 i=selected:selected=-1:gosub 610
990 score=0
1000 for i=0 to 11
1010 if selection(i)==(shuffle(i)>=6)*2 then score=score+1:cpeek &H819B else cpeek &H817E
1020 gput teamx(1)-4,targety+10*i,8,8000,white
1030 next
1040 for i=0 to 7
1050 line 0,controlly+i,320,controlly+i,black
1060 next
1070 gprint teamx(1)-28,controlly,format$("%2d / 12",score),white
1080 if inkey()==0 goto 1080
操作方法
左右キーで、選択中の (横に矢印が出ている) 球団の分類を切り替える。
上下キーで、分類を切り替える球団を選択する。
すべての球団をセ・リーグまたはパ・リーグのいずれかに分類した状態でEnterキーを押すと、解答が確定し、採点が行われる。
採点結果の画面で何かキーを押すと、実行を終了する。
実行結果例
初期画面。
球団の表示順は毎回シャッフルされる。
適当に分類してみた。
すると、下に出ている操作説明に「Enterキーで提出」が追加される。
採点を行った。
適当に分類したわりには、けっこう当たっていた。
ライセンス
今回のプログラムは、CC BY 4.0 でライセンスする。
このプログラム (改造したものを含む) を公開の場で利用する際は、出典を示していただけると嬉しい。
これは、Qiitaの利用規約に基づくプログラムの利用を妨げるものではない。
解説
10~40行目:暗号文
球団の情報をArcfourで暗号化し、一定のバイト数ごとに分割して格納している。
キリがよく、かつ文字を勝手に消される罠が発動しない、64バイトごとに分割した。
70~160行目:Arcfourの初期化
パスワードを用い、Arcfourのアルゴリズムに沿って状態を初期化する。
50・60・170~330行目:球団データの復号
配列から暗号文を取得し、Arcfourのアルゴリズムに沿って復号し、Shift_JISの定義に沿ってバイト数を判定しながら1バイトまたは2バイトを球団データ用の配列に格納していく。
球団データはLFで区切り、12球団のデータを格納し終えたら終了して次の処理に移る。
340~740行目:UIの初期化と描画用サブルーチン
球団名以外に表示する文字列、および描画用の色や位置などの情報を用意し、UIの固定部分の描画を行う。
さらに、球団の表示順をシャッフルし、球団の初期表示を行う。
ここで表示を行うコードは、後で表示の更新用のサブルーチンとしても使用する。
750~970行目:選択操作の受付
ユーザーからの選択操作を受付、操作内容に応じて状態を更新する。
さらに、状態の更新内容に応じて表示も更新する。
980行目~1080行目:採点
ユーザーの解答の採点を行い、正誤と得点を表示する。
球団データのうち、最初の6球団がセ・リーグ、残りの6球団がパ・リーグである。
最後に、終了前にキー入力を待機する。
今回新たに発見した罠
今回のプログラムの開発中、さらに罠が発見された。
1回は実行できても、2回目の実行でリセットがかかることがある
今回のプログラムの140行目を
140 j = (j + s(i) + asc(mid$(password$, i % len(password$) + 1,1))) % 256
とすると、1回は実行できるが、実行終了後もう1回実行するとなぜかリセットがかかってしまった。
「実行終了」は、終端で終了した場合でも、途中で止めた場合でも発生した。
詳しい条件は未解明である。
これまで発見していたリセットコマンドは
- 異常なパラメータを与える
- 一発で確実に効果を発揮する
という特徴があったが、今回のリセットは1回目は意図通り実行され、これらの特徴をどっちも持たない。
もちろん、意図しないリセットは開発・実験中のプログラムの消失などの被害に繋がる可能性がある。
そのため、これまでのリセットコマンドより凶悪な罠であるといえる。
消されない程度の字数の行でも拒否されることがある
行
410 black=rgb(0,0,0):white=rgb(255,255,255):upkey=30:downkey=31:leftkey=28:rightkey=29:enterkey=13:titlex=140:titley=0:questionx=68:questiony=20:groupy=40:targety=60:controllx0=76:controll1x=controll0x+8*9:controlly=190:arrowoffset=64
を入力すると、「Out of memory」と表示され、プログラムに反映されなかった。
この行は、勝手に無警告で消されることがわかっている256文字よりも短い234文字である。
エラーメッセージが出る分勝手に無警告で消される罠よりは良心的であると考えられるが、大量のプログラムを流し込んでいる途中で出ても目立たず、気付かず、混乱の原因となる。
さらに、今のところ出る条件は未解明である。
そのため、これも立派な罠だろう。
対策としては、やはり書けるプログラムの量が多いので、行数をケチりすぎず、1行を長くしすぎないことだろう。
また、プログラムを流し込む際に
uart 1,3,115200
などでメッセージのUARTへの出力を有効化しておくと、Out of memory をUARTに出してくれるので、少なくともどこかでこの罠が発動していることはわかるようになる。
これが出たら、list
で反映されたプログラムを確認し、欠けているところを探す、という運用ができる。