motemen15パズル

about motemen

美顔器 (@motemen) / Twitter

inspired by

airreader.hatenablog.com

15パズルの実装

https://ja.wikipedia.org/wiki/15%25E3%2583%2591%25E3%2582%25BA%25E3%2583%25ABja.wikipedia.org

15パズルの機能を実装するにあたり、考えるべき要素はサイズ16の配列と入れ替え操作の2つのみである。

サイズ16の配列

これは盤面そのものに対応する。

0を空のパネルとして以下の配列があるとする

[0, 1 , 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

これを盤面として表現する場合4要素ずつに区切り、縦に並べてやれば良い

[
  [ 0,  1,  2,  3],
  [ 4,  5,  6,  7],
  [ 8,  9, 10, 11],
  [12, 13, 14, 15]
]

制約付き入れ替え操作

入れ替え操作は単に選択したインデックス n に対し、空のパネルの位置と入れ替える操作を考えれば良い。

ただし、15パズルには、空のパネルと隣接するパネルのみ移動ができる、という物理的制約がある。 そのためこの制約を定義してやる必要がある。

隣接判定

空のパネルのインデックスをe とし、あるインデックス n が隣接しているかどうかを判定するためには以下の式が利用できる

abs(e-n) == 1 || abs(e - n) == 4

式の前半はパネルが横方向に隣接しているかの判定、後半はパネルが縦方向に隣接しているかの判定である。

(追記) これだけでは空パネルが端にある場合、次の行の最初の要素と入れ替えられてしまうので、次の判定が追加で必要となる。

(e % 4 == 0 && e - n !== 1) || (e % 4 == 3 && n - e !== 1)

つまり最終的な判定式は以下となる。

((e % 4 == 0 && e - n != 1) || (e % 4 == 3 && n - e != 1)) && abs(e-n) ==1 || abs(e-n) == 4

実物

https://mysticdoll.com/motemen15puzzle

実際に動いている様子はここで確認できる。

美しい色の並びですね

実際の実装

React.jsで雑に実装した。

github.com

まず以下のようなmotemenを表すデータを定義しておく

// motemen.js
export const white = "fcfff6";
export const motemen = [
  "c3d5e3", "fcfff6", "133a89", "9da7b9",
  "815226", "ffe0d3", "f8c7b6", "da2e20",
  "a4acc1", "fec4b8", "ffb3a2", "e16b5d",
  "aeb3b7", "373e44", "53200d", "595a5f"
];

そして実際に15パズルそのものを表現するReact Componentは以下のような形となる。

/// Board.jsx
import React from 'react';
import { white, motemen } from './motemen'

export default class Board extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            motemenArray: this.props.motemenRandomized
        };
    }

    sliceMotemen() {
        return [
            this.state.motemenArray.slice(0, 4),
            this.state.motemenArray.slice(4, 8),
            this.state.motemenArray.slice(8, 12),
            this.state.motemenArray.slice(12, 16),
        ];
    }

    swap(n) {
        const w = this.whiteIndex();
        if (!((w % 4 === 3 && n - w === 1) || (w % 4 === 0 && w - n === 1))) {
            const sub = Math.abs(w - n);
            if (sub === 1 || sub === 4) {
                const motemenArray = this.state.motemenArray.slice();
                motemenArray[w] = this.state.motemenArray[n];
                motemenArray[n] = this.state.motemenArray[w];
                this.setState({
                    motemenArray
                });

                if(JSON.stringify(motemen) === JSON.stringify(motemenArray)) {
                    alert('You know motemen.');
                }
            }
        }
    }

    whiteIndex() {
        return this.state.motemenArray.findIndex(c => c === white)
    }

    render() {
        return (
            this.sliceMotemen().map((m, r) => {
                    return (
                        <p style={{ lineHeight: "0px", margin: "0px" }} key={r}>
                            {
                                m.map((color, index)=> {
                                    const style = {
                                        height: "128px",
                                        width: "128px",
                                        background: `#${color}`,
                                        content: " ",
                                        display: "inline-block"
                                    };
                                    const onClick = () => this.swap(index + r * 4);
                                    return (
                                        <span key={color} style={style} onClick={onClick}/>
                                    )
                                })
                            }
                        </p>
                    )
            })
        )       
    }
}

あとはランダムにシャッフルした配列でBoardを初期化してやれば完成となる。

return (
    <div className="App">
      <main>
        <Board motemenRandomized={motemenRandomized}/>
      </main>
    </div>
  );