熱冷めやらぬショートコーディング。
そして見事にかぶりました。→ 9行のhtmlでスネークゲームを作った(Qiitaで遊べるよ!!)
あちらの方が、リセットボタンついていたりなどリッチな感じ。
別レギュです。予めご了承ください。
🐍 Snake Game!
頭(黄)を動かし、餌(赤)を食べ、体(緑)をどんどん伸ばしていこう!
操作は WASD で。リセットはリロードしてください。
CodePen の場合、右下の「Rerun」でいけます。
※CodePen だと画面内にフォーカスが無いとキー入力が効かないようです。
キー入力が効かない!って方は github.io の方で公開したのでどうぞこちらから。
shortcoding/snake6.html
See the Pen SnakeGame6 by nagtkk (@nagtkk) on CodePen.
動作チェックは Chrome/Firefox/Edge にて。
コード
<body onload="F=_=>{for(p=new Date;a[p%=n];p++);U(3,p,P[I='innerHTML']=++s+`PTS
`)};H=_=>setTimeout(_=>{u=1-(d&2);v=d&1;y=u*!v+q/w|U(9);q=u*v+q%w;~q*~y&&q<w&y<
w&a[q+=y*w]<4?H(a[q]?F():U(0,l.pop())):P[I]+='END.'},250,U(6),l=[q,...l]);U=(v,
p=q)=>c[p].style=`border:9px solid#`+'dddf10db06c3'.substr(a[p]=v,3);w=17;n=w*w
c=[a=[l=[]]];onkeydown=e=>d=~'awds'.indexOf(e.key)||d;for(s=q=-1;++q-n;U(d=0))c
[q]=(r=q%w?r:T.insertRow()).insertCell();F(H(q>>=1))"><p id=P></p><table id=T>
相変わらずの onload 実装。HTML5 でないので、p
タグはきちんと閉じています。
onload
の中身を一応展開。
展開してもわかりにくい!
F = _ => {
for (p = new Date; a[p %= n]; p++);
U(3, p, P[I = 'innerHTML'] = ++s + `PTS `)
};
H = _ =>
setTimeout(_ => {
u = 1 - (d & 2);
v = d & 1;
y = u * !v + q / w | U(9);
q = u * v + q % w;
~q * ~y && q < w & y < w & a[q += y * w] < 4 ?
H(a[q] ?
F() :
U(0, l.pop())) :
P[I] += 'END.'
}, 250,
U(6), l = [q, ...l]
);
U = (v, p = q) =>
c[p].style = `border:9px solid#` + 'dddf10db06c3'.substr(a[p] = v, 3);
w = 17;
n = w * w
c = [a = [l = []]];
onkeydown = e =>
d = ~'awds'.indexOf(e.key) || d;
for (s = q = -1; ++q - n; U(d = 0))
c[q] = (r = q % w ? r : T.insertRow()).insertCell();
F(H(q >>= 1))
文字数減らすためのアレコレのせいで、流れがバラバラです。
今回は一覧ではなく、ある程度順序がわかるように直してコメント入れてみます。
// T: テーブル
// P: パラグラフ(メッセージ表示)
s = -1; // スコア, 餌が増えるときにカウントしているので-1スタート
w = 17; // 1 辺 17 マス
n = w * w; // 全マスの個数
l = []; // list, 蛇の座標キュー, [頭...尾]
a = []; // array, マスの状態, 0=無,3=餌,6=頭,9=体
c = []; // cells, セル要素の一覧
d = 0; // direction, 入力=蛇の向き
// q; // 現在位置, cursor, cells と被るので q に。
// p; // 一時変数、主に座標
// u,v,y,r; // 一時変数
// Update, マスの更新, 座標 p を状態 v に。
U = (v, p) => {
a[p] = v;
c[p].style = `border:9px solid#` + 'dddf10db06c3'.substr(v, 3);
};
// Feed, 餌の追加とスコアの更新
F = _ => {
for (p = new Date; a[p %= n]; p++);
U(3, p);
P[I = 'innerHTML'] = ++s + `PTS `;
};
// Head, 頭の追加と移動処理
H = _ => {
U(6, q); // 現在位置を頭に
l = [q, ...l]; // 蛇に頭を追加
// 移動処理
setTimeout(_ => {
U(9, q); // 現在位置を頭から体に
// 移動先の計算
u = 1 - (d & 2);
v = d & 1;
y = u * !v + q / w | 0;
q = u * v + q % w; // x
if (~q * ~y && q < w & y < w & a[q += y * w] < 4) {
// 移動先が画面外でも体でもない時
if (a[q]) { // マスが空でない = 餌
F(); // 餌追加
} else {
U(0, l.pop()); // 尾を削除
}
H(); // 頭追加
} else {
// おしまい
P[I] += 'END.';
}
}, 250);
};
// キー入力処理
onkeydown = e => d = ~'awds'.indexOf(e.key) || d;
// 初期化処理
for (q = -1; ++q - n;) { // q = 0 => n-1
r = q % w ? r : T.insertRow(); // w 毎に行追加
c[q] = r.insertCell(); // セル追加
U(0, q); // 状態を0に
}
q >>= 1; // 開始位置 = 中央 = floor(n / 2);
H(); // 頭追加
F(); // 餌追加
ロジック的には割と素直(なはず)。
以降は、前回のマインスイーパーで書いた部分は省きつつ、それ以外の面白ポイント(個人の感想です)について。
ポイント解説
キー入力処理と移動処理
個人的なここ好きポイント。
onkeydown = e => d = ~'awds'.indexOf(e.key) || d;
onkeydown =
は window.onkeydown =
ですね。
イベントを受け取って、e.key
から文字表現を取ってきて indexOf
。
戻り値は -1,0,1,2,3
の何れかで、それをビット反転しているので、0,-1,-2,-3,-4
になります。
awds
いずれでもないときは -1
→ 0
になるので、論理演算 ||
をパスし、d
は変化なしになります。
何れかに該当した場合には、d
は -1
から -4
になりますが、
それぞれ下 2 bit が 11,10,01,00
になっているので、上下左右 4 つのパターンをここから取り出せます。
a = 1 - (d & 2); // 2 bit 目を取り出して 1 から引く => -1 or +1
b = d & 1; // 1 bit 目を取り出す => 0 or 1
dx = a * b;
dy = a * !b; // !b は 0=>false=>true=>1 or 1=>true=>false=>0
これで (dx,dy)
は (-1,0),(0,-1),(1,0),(0,1)
になり、座標の移動を表現できます。
おさまりが良くて好きなパターン。
座標の判定
蛇が範囲外や自分の体とぶつかっていないかの条件式は、以下のようになっています。
if (~q * ~y && q < w & y < w & a[q += y * w] < 4) ...
q
を使い回していて分かりにくいので、書き直すと以下のようになります。
q = y * w + x;
if(~x * ~y && x < w & y < w & a[q] < 4) ...
まず、右端の a[q] < 4
は、蛇の体でないことを確認しています。
餌の時は 3
、空の時は 0
なので、4
未満。
x < w
と y < w
は見たまんまですね。
今回縦幅は横幅と同じなのでどちらも w
。
左端の ~x * ~y
ですが、これは x >= 0 && y >= 0
とほぼ同じです。
順を追ってみていくと。
x >= 0 && y >= 0
!(x < 0) && !(y < 0)
- 蛇は 1 マスずつしか動かない = 負になるときは必ず
-1
!(x == -1) && !(y == -1)
!(!((-1)-x)) && !(!((-1)-y))
!(!(-x-1)) && !(!(-y-1))
!(!~x) && !(!~y)
~x && ~y
- どちらか一方が 0 のとき、結果が falsy(=0) になればよいので
~x * ~y
といった具合。
あとは、短絡が要らないので &&
の代わりに &
を使って完成です。
ただし ~x * ~y
の隣の &&
は &
にできません。
0
でないからと言って 1
だとは限らないので、真の時に結果がおかしくなります。
というわけで &&
噛ませて、掛け算の結果を一旦 boolean に変換しています。
キュー
今回蛇の体の座標をキュー l
で管理しています。
蛇の移動は、キューに頭を追加して末尾を取り出すということ。
餌を食べて体が伸びるときは、頭だけ入れて末尾を取り出さなければ ok。
さて、JavaScript の配列は、以下の二つのメソッドのペアを使えばキューとして扱えます(効率はさておき)。
a=[];
a.unshift(p);a.pop();
a.push(p);a.shift();
ショートコーディング的には、長さ的に push+shift
が有利...というのは過去の話。
a.unshift(p);a.pop();
a.push(p);a.shift();
a=[p,...a];a.pop(); // New!
スプレッド構文+pop が良さそうです。
というわけで今回はそのように。
乱数
for (p = new Date; a[p %= n]; p++);
関数 F
の中で、餌を置くための空の座標 p
を探しています。
今回は、短さを優先して Math.random()
ではなく new Date
を採用。
Date オブジェクトは、数値に変換するとミリ秒を返すので、その余りを使っています。
setTimeout
が基本的にばらつくので偏りもさほど気になりません。
もし空でなければ、単に空になるまで座標をずらします。
後半になると、体の横とかきわどいところに餌が出やすくなりますが、
まあ、難易度的にちょうどよいかなと。
なお、蛇の目の前に常に餌が出続け、餌と蛇で全マス埋まるというミラクルが発生すると、無限ループに陥ります。
Math.random()
でなくnew Date
を使っていることを考慮しても、確率的にまず起こらないので、今回対処していません。
無限ループが発生したら完全クリアです。おめでとう!
ちなみに、以下のように書けばさらに短くなります。
for(;a[p=new Date%n];);
当然ながら値が変わるのは 1ms 毎なので、あたりを引くまでビジーループです。
さすがに採用しませんでした。
何だっていい!整数にするチャンスだ!
整数の割り算をするのに、よくこういうパターンを使います。
y=p/w|0;
JavaScript での割り算は浮動小数点数になってしまうので、ビット演算を噛ませて整数にしているわけですね。
もちろん、次のようにも書けます。
y=p/w|NaN;
y=p/w|'';
y=p/w|undefined;
y=p/w|false;
y=p/w|null;
y=p/w|{};
y=p/w|console.log('to int!');
整数に変換したときに 0
なるなら、なんだって良いわけです。
NaN だって...いや NaN でもない。
これを意識しておくと、前後の式と組み合わせて、文字数が結構短縮できることがあります。
F(); // 例えば undefined を返す関数
y=p/w|0;
y=p/w|F(); // 2 文字短縮
今回は移動処理のところで使っています。
マスの表示
マスの色を変えているのはこの部分。
c[p].style = `border:9px solid#` + 'dddf10db06c3'.substr(v, 3);
背景(background
)ではなくて border
で色を付けています。
background
で色を付けようと思うと、サイズも指定しないといけないので、どうしても長くなります。
c[p].style = `width:20px;height:20px;background:#` + 'dddf10db06c3'.substr(v, 3);
加えて、前回のマインスイーパーで少し話しましたが、HTML5 か否かでテーブルセルの height
の扱いが異なるので、両対応できれいな正方形にするのが難しかったりします。
今回、セルには色を塗るのみで文字を入れる必要がないため、HTML5 にせず border
で塗ることにしました。
真ん中に小さな空白が出てしまうのがやや難点ですが、ゲーム自体に影響はなかろうと妥協。
CodePen 等 HTML5 として扱われてしまう環境でも、見た目に大差ありません。
マス同士は区切りが見えていた方がゲーム的に良いと思ったので、border-collapse
も指定せず。
色の選択
色は、状態の値(0,3,6,9
)を使って文字列から切り出しています。
というか、文字列から切り出したいがために飛び飛びの値になっています。
ためしに配列版と比較してみると、こんな感じ。
'dddf10db06c3'.substr(v,3) // v = 0,3,6,9
['ddd','f10','db0','6c3'][v] // v = 0,1,2,3
その他、v に適当な演算にして三桁の数にすることも考えましたが、短く書ける適当な値が見つからず、加えて.toString(16)
が長いので断念。
10進数のままだと、全体が暗い色になってしまうので、ゲーム自体が見えにくくなりこれも棄却。
文字列を一部オーバーラップさせれば数文字削れるなあとも思いつつ、
行も減らないし、色は後からいじりたくなるかもしれないし、ということで現在の形に収まりました。
solid
と #
の間に空白がありませんが css の文法上問題ありません。
#
で、トークンが区切られます(今回たまたま気づいた)。
ちなみにカラーリングの元ネタはこちら。
スネークゲームとは。
まとめ
さて、これで大体話し終えたような気がします。何かあればコメントで。
今回は、前回の記事を反省をしつつ、ちょいと長めに、考えた中身をそれなりに書き出してみました。
個人的な思いになりますが、ショートコーディングの面白さは、短くすることそのものよりも、その過程で出てくる普段使わない発想とか気づきにある気がします。
短くすること自体は、建前とは言いませんがあくまでも方向性で、
スポーツに勝敗依らず良い試合がありますように、
ショートコーディングも、最短でなくとも、
それぞれに面白い部分があるように思っています。
自分で書かなくても、見ているだけでも結構楽しい。
つまり何が言いたいかと言うと、
みんなもショートコーディングしよう! (沼への誘い)
おまけ: 1行の JavaScript でズンドコキヨシ
console.log((F=n=>Math.random()<.5?'ドコ'+(n>3?'キ・ヨ・シ!':F()):'ズン'+F(-~n))())
はい。76文字ですが半角カナ使ってるのでレギュレーション違反です。
ちなみに、ちゃんと全角にすると SJIS 換算で 84 byte。
console.log((F=n=>Math.random()<.5?'ドコ'+(n>3?'キ・ヨ・シ!':F()):'ズン'+F(-~n))())
割といいところまで行っている気もしつつ、元ネタでは「出力し続けて」と言っているので、最後に出力している点がやっぱりレギュレーション違反な気がします。
あと最悪スタックが溢れます。
まあ、そこはさておき、これの面白い点は n
が
undefined
→ 1
→ 2
→ 3
→ ...
とかいう気持ち悪い変則的な変化をしている点。
0
から始めなくたっていい。
undefined
から始めたっていい。
JavaScript には、そんな自由がある。
(※用法用量を守って正しくお使いください)