32
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

5行のHTMLで15パズル作ってみた(追記:3行まで短縮)

Last updated at Posted at 2021-11-28

概要

HTMLとJavaScriptを用い,「15パズルを実装する」というお題でコードゴルフ(ショートコーディング)をしてみました.

15パズルとは

15パズルは、スライディングブロックパズル(Sliding puzzle)のひとつである。4×4のボードの上に4×4-1すなわち15枚の駒があり、1駒ぶんの空きを利用して駒をスライドし、駒を目的の配置にする。(Wikipediaより)

要するに数字を動かして順番にそろえるコレです.
image.png

レギュレーション

  • 1行79文字
  • 全角文字は1つにつき2文字とカウントする

基本的には,2ちゃんねるで行われていた「7行プログラミング」に準ずる形で設定しています.

遊び方

キーボードの矢印キーで,空きマスに隣接している数字を指定した方向に動かします.
数字の配置が
image.png
のようになればゲームクリア.

実行環境

  • 言語 : HTML, JavaScript
  • 動作ブラウザ : Google Chrome, Microsoft Edge

他ブラウザでの動作確認はぶっちゃけめんどくさくてやっておりません.情報求む.

完成コード

<body id=D onKeyDown=F(event.which-37) onLoad='A=[];g=f=0;for(i=16;i--;A.push(i))
;s="①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮ ";F=(K)=>{if(!f&&0<=K&&K<4){a=A.indexOf(15);[
M,N]=K%2?[--K?a<12:a>3,a+~-K*4]:[(a+!!K)%4,a+K-1];M&&([A[a],A[N]]=[A[N],15]);S=""
;for(k=16;k--;S+=s[A[k]]+(k%4?"":"<br>"));if(g)for(f=1,r=16;--r;)f&=(A[r-1]>A[r])
}D.innerHTML=S+(f?"Win":"")};for(j=999;j--;)F(4*Math.random()|0);g=1'>

418バイト.スクリプト本体は全てonLoad属性にぶっこんでいます.

解説

スクリプト本体をonLoad属性から出して,コードに適宜改行,スペースとコメントなどを挟んだものがこちらになります.

<body id=D onKeyDown=F(event.which-37)><script>
// A : 盤面配列 0~15の数値を格納
// 0~14 が丸数字 ①~⑮ に対応,15 は空きマス
A = [];

// g : ゲーム中か(初期化中でないか)否かのフラグ
// f : ゲームクリア判定のフラグ
g = f = 0;

for(i = 16; i--; A.push(i));  // 盤面 A に 0~15 の数値を逆順で格納
s = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮ ";      // s : 盤面表示用丸数字テキスト

// F : キー入力を受け取り,盤面を更新し,ブラウザに描写する関数
F = (K) => {
    if(!f && 0 <= K && K < 4){  // 未クリア & 入力されたキーが矢印キーならば
        a = A.indexOf(15);  // a : 空きマスの位置

        // M : 入力した方向に数字が動かせるかのフラグ
        // N : 動く対象の数字の位置
        [M, N] = K%2 ? [--K ? a<12 : a>3, a+~-K*4]
                     : [(a+!!K)%4, a+K-1];

        M && ([A[a], A[N]] = [A[N], 15]);  // 空きマスと数字の位置を交換

        S = "";  // S : ブラウザに表示するテキスト 盤面その他を格納
        for(k = 16; k--; S += s[A[k]] + (k%4 ? "" : "<br>")); // 盤面を格納

        if(g){  // ゲーム中ならば
            for(f = 1, r = 16; --r;){
                f &= (A[r-1] > A[r]);  // クリア判定を行い,f を更新
            }
        }
    }
    // ブラウザに盤面を表示,クリアしたら "Win" の文字列も追加
    D.innerHTML = S + (f ? "Win" : "");
}

for(j = 999; j--;) F(4*Math.random() | 0);  // 盤面初期化
g = 1;  // 初期化終了,g を更新(フラグをゲーム中に設定)
</script>

上から順番に見ていきましょう.

<body id=D onKeyDown=F(event.which-37)>

ブラウザ内でキーが押下されたとき(onKeyDown),押下されたキーのキーコード(event.which)から37を引いたものを取得して,関数Fに投げています.
矢印キーの←,↑,→,↓のキーコードがそれぞれ 37, 38, 39, 40 なので,37 を引くことで 0, 1, 2, 3 に変換し,扱いやすさ向上&コード短縮に役立てています.


g = f = 0;

gはゲームの最中であるか(盤面の初期化の最中ではないか)を示すフラグ,fはゲームをクリア済かを示すフラグです.
どちらも0すなわちfalseで初期化しています.


for(i = 16; i--; A.push(i));

盤面配列Aに 0 から 15 までの整数を逆順で格納しています.
このfor文の表記ですが,変数iの値を 16 で初期化した後に条件式内でiのデクリメントを行っています.これは,i=16から条件式がfalse,すなわちi=0になるまでiを減算し続ける,という実装であり,

for(i = 15; i >= 0; i--) A.push(i);

という実装と同様の動作をします.


次に,関数F(K)の内部を見ていきましょう.

if(!f && 0 <= K && K < 4){ ...

先述の通り,fはゲームクリア済みか否かを示すフラグであり,0ならば未クリア,0以外ならばクリア済みであることを表します.
関数Fの引数であるKは先述した (キーコード-37) の値ですから,このif文は,ゲームが未クリア,かつ入力が矢印キーからならば(i.e. Kが 0, 1, 2, 3 のいずれかならば)実行されます.


a = A.indexOf(15);
[M, N] = K%2 ? [--K ? a<12 : a>3, a+~-K*4]
             : [(a+!!K)%4, a+K-1];

ここで出てくる変数の意味は以下の通り.

  • a : 盤面中の空きマスのある位置(0 ~ 15 の整数)
  • M : 矢印キー押下時に,指定された方向への数字の移動が可能かどうかのフラグ(0で移動不可能,0以外で移動可能)
  • N : 移動可能な場合,移動する対象の数字がある場所(0 ~ 15 の整数)

M, Nへの代入部は複雑なので,if文で書き換えてみます.

if(K % 2){
    if(--K) M = (a < 12);
    else    M = (a > 3);
    N = a + ~-K * 4;
}else{
    M = (a + !!K) % 4;
    N = a + K - 1;
}

if文は,押下された矢印キーに応じて次のように動作します.

  • 上下キー(K=1 or 3) : 1 -> true -> 前半ブロック実行
  • 左右キー(K=0 or 2) : 0 -> false -> 後半ブロック実行

キー入力が上下キーの場合,さらに--Kの値,すなわち上下のどちらが入力されたかが評価され,Mの値は次のように設定されます.

  • 上キー(K=3) : --K=2 -> true -> M = (a < 12);
    • 空きマスの位置が最下行以外 (a < 12) のとき,数字をより上行へ移動可能
  • 下キー(K=1) : --K=0 -> false -> M = (a > 3);
    • 空きマスの位置最上行以外 (a > 3) のとき,数字をより下行へ移動可能

Nへの代入時における~-KK-1と同値であり,演算子~, -の優先度が*よりも上であることから,コード短縮テクニックとして頻繁に使用されます(K+1と同値である-~Kも同様).
また,Kの値は先程デクリメントした値からさらに -1 されることから,Nに代入される値は以下の通りになります.

  • 上キー('K'=3) : ~-K=1 -> N = a+4 (空きマスの 1 行下にある数字が移動)
  • 下キー(K=1) : ~-K=-1 -> N = a-4 (空きマスの 1 行上にある数字が移動)

キー入力が左右の時は以下の通り.

  • 右キー(K=2) : !!K=true -> !!K=1 -> M = (a+1)%4, N = a+1
    • 空きマスが左端以外の列にあるとき,空きマスの 1 列左にある数字が移動
  • 左キー(K=0) : !!K=false -> !!K=0 -> M = a%4, N = a-1
    • 空きマスが右端以外の列にあるとき,空きマスの 1 列右にある数字が移動

ここでは,否定演算子!を 2 回重ねて使うことで,0ならば0,それ以外ならば1という変換を行っています.


S = "";
for(k = 16; k--; S += s[A[k]] + (k%4 ? "" : "<br>"));

ここは盤面のテキスト化です.ここでも先述の短縮forループを利用しています.
盤面Ak番目の要素について,それに対応する丸数字をとってきてSに加算しています.
kが 4 で割り切れる,すなわち行末であるときに改行も加算します.


if(g){
    for(f = 1, r = 16; --r;){
        f &= (A[r-1] > A[r]);
    }
}

ゲームの最中(g==true)であれば,if文内部でクリア判定を行い,フラグfを更新します.
ここでは,いったんf1(クリア済)で初期化した後,盤面配列Aの要素が昇順で並んでいれば,ループが終わったときにもftrueに保たれる(otherwise false),という実装にしています.
ここでも先程の短縮forループを利用していますが,条件式のデクリメントが前置されているのは,rが 0 のときにループを抜けてほしいため(r-1に負になってほしくない)です.


D.innerHTML = S + (f ? "Win" : "");

盤面をテキスト化した文字列Sをブラウザに出力します.
クリアしていれば(i.e. f==true)“Win”の文字列も一緒に出力します.
これで関数F(K)の定義は終了です.


for(j = 999; j--;) F(4*Math.random() | 0);
g = 1;

最後に,ゲームの初期化処理を行っています.
Fに 0, 1, 2, 3 のいずれかの数値を引数として代入して実行する処理を 999 回繰り返しています.
すなわち,「とりあえずランダムな方向に数字を(最大)999 回動かす」という処理を行っています.
先程クリア判定のためにフラグgを用意しているのは,この盤面の初期化時にも関数Fを利用しているからであり,この時点でクリア判定が行われてしまうのを防ぐという目的があります.
そして,初期化後にgtrueに設定して,ゲーム中フラグを立てます.

最後に

とりあえず 5 行に収めることには成功しましたが,もっとスマートな実装ができないものかという模索は今後も気が向いたときにやっていきたいなと思う所存であります.
コードゴルフ楽しい!

追記

Qiita の皆様の叡智をお借りして,3 行半以下まで短縮されました(コードと更新履歴はコメント欄を参照).
心より感謝申し上げます.

追々記

またもや皆様の叡智をお借りして,ついに 3 行以内に収まりました(コメント欄参照).
本当にありがとうございます.

32
19
14

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?