はじめに
「200行のVue.jsでスネークゲームを作った」、「たった7行でテトリスを実装「七行プログラミング」とは」の影響を受けて「23行のhtmlでマインスイーパーを作った(Qiitaで遊べるよ!!)」という記事を書いたのだが、トレンド入りしたり影響を受けた記事が作られたりするなど個人的には反響が大きかったように思える。
そこで今回はショートコーディング第二弾として、スネークゲームを可能な限り小さいhtmlで制作した。
最終的に行数はたった1桁の9行、サイズは1KBを切るスネークゲームの完成に成功した。
さあ、スネークゲームで遊ぶのだ!
See the Pen HTML_Snake_Min by T.D (@td12734) on CodePen.
大きい画面で遊びたい人はこちら
Qiitaだと上下移動でスクロールされてしまうので大画面推奨です。
ゲームルール
- ヘビ(緑色)を動かしてエサ(赤色)を取得するゲームです。
- 矢印キーを押すとヘビの体を操作できます。
- 一度上の枠内をクリックしてからでないと矢印キーが反応しないかもしれません。
- 最初に矢印キーを押した後、ヘビが動き始めます。
- ヘビは止まらずに動き続けます。
- エサを取るとヘビの体が伸びます。
- ヘビの頭が胴体に衝突する、盤外に行くとゲームオーバーです。
-
Reset
ボタンを押せばゲームをやり直せます。
プログラム
コード
ゲームは以下のコードで実装した。七行プログラミングのルールに従い、1行は79文字以下としている。
<!doctype html><body onKeyDown=K=event.keyCode-36 onload='L=f=>{i=t;while(i--)f
(C(i))};C=c=>T.rows[c/w|0].cells[c%w];D=_=>L(c=>c.style.background=!c.v?0:!~c.v
?"red":c.v-s?"tan":"lime");F=_=>(c=C(M())).v?F():c.v--;M=_=>Math.random()*t|0;R
=_=>{L(c=>c.v=P[I="innerHTML"]=0);C(p=M()).v=s=1;D(F(p++))};for(i=K=0;i<(t=400)
;i++){i%(w=20)?0:r=T.insertRow(0);r.insertCell(0).style="width:24px;height:8px"
+";border:solid"}R(setInterval(_=>{if(+P[I]>=P[I]&&K){if((K>3&&(p+=w)?p<=t:K>2
&&p++?p%w-1:K>1&&(p-=w)?p>0:p--&&p%w)&&C(p-1).v<1){C(p-1).v&&!F(P[I]=s++)?0:L(
c=>c.v>0&&c.v--);D(C(p-1).v=s)}else P[I]+=" Gameover"}},200))'><input onclick=
R() type=button value=Reset><p id=P><table id=T style=border-collapse:collapse>
htmlで保存してブラウザで開いても恐らく遊べます。
最終行を始めとするほぼ全ての行が79文字埋まっており、視覚的にも大変美しいと思います。
主な実装はbodyのonloadに書いてあるが、何書いてあるか分からないと思うのでonloadを書き下したコードを以下に貼る。
L=f=>{i=t;while(i--)f(C(i))};
C=c=>T.rows[c/w|0].cells[c%w];
D=_=>L(c=>c.style.background=!c.v?0:!~c.v?"red":c.v-s?"tan":"lime");
F=_=>(c=C(M())).v?F():c.v--;
M=_=>Math.random()*t|0;
R=_=>{
L(c=>c.v=P[I="innerHTML"]=0);
C(p=M()).v=s=1;
D(F(p++))
};
for(i=K=0;i<(t=400);i++){
i%(w=20)?0:r=T.insertRow(0);
r.insertCell(0).style="width:24px;height:8px;border:solid"
}
R(setInterval(_=>{
if(+P[I]>=P[I]&&K){
if((K>3&&(p+=w)?p<=t:K>2&&p++?p%w-1:K>1&&(p-=w)?p>0:p--&&p%w)&&C(p-1).v<1){
C(p-1).v&&!F(P[I]=s++)?0:L(c=>c.v>0&&c.v--);
D(C(p-1).v=s)
}
else P[I]+=" Gameover"
}
},200))
改行しても23行しかない。
ただ、見やすくなったもののコードの解読は困難だと思うので以下で説明を行う。
変数、関数の説明
コード圧縮の都合上、全ての変数名と関数名は1文字になっている。
その結果、可読性が大幅に失われたのでここでは変数名、関数名について説明する。
変数、関数名
- 1文字に圧縮する前に付けたであろう名前
- この行以降は動作説明など
グローバル変数
i
- 特に無し
- ループ調整変数
p
- player
- ヘビの頭の座標+1
r
- tableRow
- テーブルの行
t
- tableSize
- テーブルのマスの数、及びsetTimeoutのインターバル
w
- width
- テーブルの横サイズ
I
- innerHTML
- 定数"innerHTML"
K
- keyCode
- 入力したkeyCodeから36を引いたもの
P
- pElement
- スコアとGameOverを表示するテキストエリア
T
- tableElement
- ゲームの盤面
ローカル変数
c
- cell
- テーブルのセル
f
- function
- Lで実行する関数
v
- cellValue
- そのセルの数値
- 0:何もない
- 1以上:ヘビの体
- -1:エサ
_
- 特に無し
- アロー関数の()を省略するために定義したダミー変数
関数
C
- GetCell
- テーブルのセルを取得する
D
- DrawCell
- ゲームの盤面のマスに色を塗る
F
- FoodSet
- 盤面にエサを設置する
L
- LoopCell
- 全てのセルに対して引数の関数を実行する
M
- Math_random
- エサとヘビの位置をランダムに決める
R
- ResetGame
- ゲームの盤面を初期化する
動作、工夫点の説明
説明を見る前に最低限知っておきたい知識
- 変数宣言の
var
やlet
は省略可能 - 変数宣言や計算などは文中でも可能
- 場所によってはカッコで括る必要がある
- if-else文より三項演算子を使った方が短くなる場合がほとんど
- if(条件) 処理1 else 処理2 → 条件?処理1:処理2
- 0は
false
、0以外はtrue
と判断される - -1をビット反転させれば0になる(~-1=0)
- 関数宣言はfunctionよりもアロー関数を使った方が短い
-
function func(a){}
→func=a=>{}
-
function (){}
→_=>{}
(「_」はダミー変数) - if,while,アロー関数などの
{}
の中の処理が1つの時、{}
を省略可能
html部分
<!doctype html>
<body onKeyDown=K=event.keyCode-36 onload='省略'>
<input onclick=R() type=button value=Reset>
<p id=P>
<table id=T style=border-collapse:collapse>
html5ではp
,table
などの閉じタグが無くても動作するのでこれらを削除する。
ただ、環境によってはhtml5と認識されずにエラーが出るので<!doctype html>
を書く必要がある。
キーボードのキーが押された時、bodyのonKeyDownが呼ばれて変数Kにキーコード-36が代入される。
(←,↑,→,↓)のキーコードは(37,38,39,40)なので、あらかじめ36を引けばKには(1,2,3,4)が代入されてKの比較時などに文字数を削減可能。
JavaScriptはscript
タグではなくbodyのonloadに全て突っ込む。
こうすれば<script>
と</script>
の文字列を省略可能で、全てがload時に実行されるのでwindow.onload
も省略可能で文字数を大幅に削減できる。
ちなみに</script>
は省略できない。
input
タグではResetボタンを定義し、押した時に関数R
を呼び出すようにしている。
p
タグでは点数とGameoverの文字を表示する。
idをP
にし、他にP変数や関数を定義しなければgetElementById()
をしなくてもP.~
の形でpタグにアクセス可能。
同様の事をtableでもやっている。
ゲームの盤面はtable
タグで表示している。
styleのborder-collapse
をcollapse
にしなければ枠線が表示されず、ゲームの難易度が跳ね上がる。
ゲームの盤面を設定する
for(i=K=0;i<(t=400);i++){
i%(w=20)?0:r=T.insertRow(0);
r.insertCell(0).style="width:24px;height:8px;border:solid"
}
真ん中よりやや下にある部分だがここが最初に実行される。
1行目はfor文で0~399まで回し、ついでにKに0、tに400を代入している。
2行目ではiを20で割った余りが0のとき、insertRow
して行を追加している。
本来は以下に示すif文だが、三項演算子を用いる事で文字を削減できる。
また0がfalse扱いされることを利用して!==0
も省略している。
if(!(i%(w=20)!==0))r=T.insertRow(0);
3行目ではテーブルのセルのスタイルを設定している。
セルを横長な長方形にし、境界線を1本だけ引くようにして盤面を見やすくしている。
ゲームテーブルの特定のセルを取得する
C=c=>T.rows[c/w|0].cells[c%w];
座標が引数cであるセルを返している。
分かりやすく書くと以下と同様の処理をしている。
function C(c) {
return T.rows[Math.floor(c/w)].cells[c%w];
}
小数点以下の切り捨てはMath.floor()
を使うよりもビット演算して|0
する方が断然短い。
ゲームテーブルの全てのセルに対し、セルを引数とする関数を実行させる
L=f=>{i=t;while(i--)f(C(i))};
テーブルの全てのセルに対し、セルを引数とする関数fを実行させている。
全てのセルの呼び出しはforよりもwhileの方が短く書ける。
ゲームテーブルに色を塗る
D=_=>L(c=>c.style.background=!c.v?0:!~c.v?"red":c.v-s?"tan":"lime");
この1行で色塗り処理を行っている。
読みやすく書き直すと以下のようになる。
function D() {
L(function (c) {
if (c.v===0) {
c.style.background="white";
}
else if (c.v===-1) {
c.style.background="red";
}
else if (c.v-s!==0) {
c.style.background="tan";
}
else {
c.style.background="lime";
}
});
}
if文は三項演算子で簡略化し、条件式は0がfalse、0以外がtrue、-1のビット反転が0でfalseである事を利用して文字数を減らしている。
色について、whiteは0で代用可能なので0を代入している。
エサは通常は赤色で示し、redは3文字しかないのでredにした。
ヘビの頭は通常は緑色で示されるが、greenは5文字で長いので4文字のlimeで代用した。
ヘビの胴体は3文字の色であるtanにした。
プレイヤーやエサを配置するセルの座標をランダムに決める
M=_=>Math.random()*t|0;
ランダムな座標、つまり0から399までのランダムな整数を返している。
切り捨てはビット演算で行っている。
何も無いランダムな座標にエサを配置する
F=_=>(c=C(M())).v?F():c.v--;
分かりやすく書くとこうなる。
function F() {
c = C(M());
if (c.v!==0) {
F();
} else {
c.v--;
}
}
まず、セルcをランダム決めている。
次にセルcの値が0ではない、つまりエサかヘビの頭か胴体ならFを再度呼び出し、0ならセルの値から1を引いて-1にしてセルをエサ扱いする。
ゲームを初期化する
R=_=>{
L(c=>c.v=P[I="innerHTML"]=0);
C(p=M()).v=s=1;
D(F(p++))
};
関数Rで全てのセルのリセット、スコアのリセット、プレイヤーのランダム配置、エサのランダム配置を行っている。
分かりやすく書くと以下のようになる。
function R() {
L(function (c) {
c.v=0;
P[I="innerHTML"]=0
});
C(p=M()).v=s=1;
p++;
F();
D();
}
最後のD(F(p++))
について、関数FとDは引数を参照しないのでどのような引数を入れても処理に支障はない。
そこでFの引数でp++を行い、Dの引数で関数F(p++)を呼び出してセミコロンを減らしている。
200ミリ秒ごとに行う処理を記述し、ゲームの初期化を行う
R(setInterval(_=>{
if(+P[I]>=P[I]&&K){
if((K>3&&(p+=w)?p<=t:K>2&&p++?p%w-1:K>1&&(p-=w)?p>0:p--&&p%w)&&C(p-1).v<1){
C(p-1).v&&!F(P[I]=s++)?0:L(c=>c.v>0&&c.v--);
D(C(p-1).v=s)
}
else P[I]+=" Gameover"
}
},200))
恐らくこのソースコードで最も難解な場所である。
分かりやすく書くと以下のようになる。
setInterval(function (){
if(Number(P[I])>=P[I]&&K!==0){ //pタグ内の文字列を数字に変換可能で、Kが0ではない時
var notSnakeCollided; //ヘビが衝突していなければtrue、説明用変数
if (K>3) { //下に移動
p+=w;
notSnakeCollided=p<=t;
}
else if (K>2) { //右に移動
p++;
notSnakeCollided=p%w-1;
}
else if (K>1) { //上に移動
p-=w;
notSnakeCollided=p>0;
}
else { //左に移動
p--;
notSnakeCollided=p%w;
}
if (notSnakeCollided&&C(p-1).v<1) { //ヘビが壁に衝突しておらず、移動先がヘビの胴体ではない時
if (C(p-1).v!==0) { //セルの値が0ではない、つまりエサの時
s++;
P[I]=s;
F();
} else {
L(function (c) {
if (c.v>0) {
c.v--;
}
});
}
C(p-1).v=s;
D();
} else { //敗北条件を満たした時はpタグの文字列にGameoverを追加する
P[I]+=" Gameover";
}
}
},200);
R();
setIntervalでは200ミリ秒ごとに以下の処理をやらせている。
- ゲームオーバーでは無く、ゲームがスタートしているなら次に進む
- Kの値によってヘビを移動させ、壁やヘビに衝突していないか確認する
- 衝突していないとき、以下の処理を行う
- 移動先がエサのとき、スコアを1増やしてエサを再配置する。
- 移動先がエサではないとき、ヘビのセルの値を1減らす。
- 移動先の座標をヘビの頭にする
- テーブルに現在の状態を反映させる
- 衝突していたとき、pタグの文字列に
Gameover
を追加する