点灯パズル

もう20年も前の事になりますが、子供の頃、
現在のタカラトミーが2000年に発売したライツアウト※に熱中していました。

これは、どういうゲームかと言うと、一つのマス目を押すと、
そのマス目と共に、上下左右のマスのランプが消⇔点へと反転するものです。
例えば4×4マスであれば、全て点灯している状態から、
全て反転させるのは容易ですが、
それが5×5になると、一気に難易度が跳ね上がります。

ただ、言葉で説明するのは難しいので、ホームページに組み込める形で、
4×4のライツアウトを参考にパズルをJavaScriptで作りました。
「ライツアウト」は恐らく商標だと思うので、
本家様と区別する為に「点灯パズル」という表現を使わせて頂きます。

4×4パズル

スタート
リセット

クリア!

0手かかりました

点灯パズルの難易度

このパズルは消→赤→緑と2回色が変わり、3回同じ場所を押すと元に戻ります。
つまり、ある個所を1回押した場合、同じ個所を2回ずつ押すと元に戻る法則があります。
これが、12手以上になると、より難しくなります。

ライツアウトの難易度

本家のライツアウトは5×5マスであり、非常に多くのステージがあり、
全面クリアには遠く及びませんでした。
後半ステージほど手数が増え、押し方が何通りと、指数関数的に増える為、
体感難易度は物凄く上がります。

ライツアウトの面白かった点

このライツアウトには様々なモードがあり、うろ覚えですが、
自分でステージを作って遊ぶというものがありました。
これは、作った問題に解があるか判定するプログラムが搭載されていますが、
極めて高度な数学的知識が必要とされると思われます。

価格は、当時最先端だったゲームボーイ・アドバンスのカートリッジより若干安く、
スーパーで売っている電子ゲームより高級でした。

もう少し大切に遊んでいたら、価値が付いたかも知れませんが、
当時仕入れた知識を元手に、仕事をさせて頂いている事は感慨深いものがあります。

配列を利用したプログラム

オセロゲームでは2次元配列を用いた方が理解が簡単ですが、
4×4パズルでは16マスしかない為、1次元配列で設計しました。
また、これはCSSがグリッドレイアウトで、
querySelectorAll()メソッドで全てのドットを取得すると、
1次元の配列となるからです。

配列のインデックスは0~15で、第1行は、16の平方根である4で割った商が1未満、
第4行は3以上、第1列は必ず4で割り切れ、第4列は余りが必ず3となります。
これは、上下左右の4方向に、それぞれドットが存在するのか判定するのに使います。

あとは赤を1、緑を2の符号に置き換え、
if文によって0→1→2→0とループするように書きます。

HTML(WordPress)


<!-- wp:columns {"className":"lights-out"} -->
<div class="wp-block-columns lights-out"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:html -->
<form>
    <fieldset>
        <div>
          <input type="radio" name="options" value="3" checked>
          <label>6手  難易度:★</label>
        </div>
      
        <div>
            <input type="radio" name="options" value="4">
            <label>8手  難易度:★★</label>
        </div>
    
        <div>
            <input type="radio" name="options" value="5">
            <label>10手 難易度:★★★</label>
        </div>
    </fieldset>
</form>
<!-- /wp:html -->

<!-- wp:columns {"className":"btns"} -->
<div class="wp-block-columns btns"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:html -->
<div class="button-area">
<span id="start-btn" class="jaja-btn btn--red btn--radius btn--cubic" target="blank" rel="noopener">スタート<i class="fas fa-angle-right fa-position-right"></i></span>
</div>
<!-- /wp:html -->

<!-- wp:html -->
<div class="button-area">
<span id="reset-btn" class="jaja-btn btn--red btn--radius btn--cubic" target="blank" rel="noopener">リセット<i class="fas fa-angle-right fa-position-right"></i></span>
</div>
<!-- /wp:html --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:html -->
<div class="boad">
    <div class="d-0"></div>
    <div class="d-1"></div>
    <div class="d-2"></div>
    <div class="d-3"></div>
    <div class="d-4"></div>
    <div class="d-5"></div>
    <div class="d-6"></div>
    <div class="d-7"></div>
    <div class="d-8"></div>
    <div class="d-9"></div>
    <div class="d-10"></div>
    <div class="d-11"></div>
    <div class="d-12"></div>
    <div class="d-13"></div>
    <div class="d-14"></div>
    <div class="d-15"></div>
</div>
<!-- /wp:html -->

<!-- wp:html -->
<div class="message">
<p id="clear-judge">クリア!</p>
<p><span id="count-number">0</span>手かかりました</p>
</div>
<!-- /wp:html --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->
 

CSS(ボタンを除く)


.lights-out .boad {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr 1fr 1fr;
    grid-auto-columns: 1fr;
    grid-auto-flow: row;
    grid-template-areas:
    "d-0 d-1 d-2 d-3"
    "d-4 d-5 d-6 d-7"
    "d-8 d-9 d-10 d-11"
    "d-12 d-13 d-14 d-15";
    width:200px;
    margin:0 auto;
}

.lights-out .boad > div{
	height:50px;
	display:flex;
	align-items:center;
	justify-content:center;
	transition:transform .3s;
}

.lights-out .boad > div:hover{transform:scale(1.5);}

.lights-out .boad > div::before{
	content:"";
	width:30px;
	height:30px;
	border-radius:50%;
	border:solid #000 2px;
	display:block;
}

/*
.lights-out .boad > div.reversible::before{background-color:gray;}
*/

.lights-out .boad > div.red::before{background-color:red;}
.lights-out .boad > div.green::before{background-color:green;}

.lights-out .boad .d-0 { grid-area: d-0; }
.lights-out .boad .d-1 { grid-area: d-1; }
.lights-out .boad .d-2 { grid-area: d-2; }
.lights-out .boad .d-3 { grid-area: d-3; }
.lights-out .boad .d-4 { grid-area: d-4; }
.lights-out .boad .d-5 { grid-area: d-5; }
.lights-out .boad .d-6 { grid-area: d-6; }
.lights-out .boad .d-7 { grid-area: d-7; }
.lights-out .boad .d-8 { grid-area: d-8; }
.lights-out .boad .d-9 { grid-area: d-9; }
.lights-out .boad .d-10 { grid-area: d-10; }
.lights-out .boad .d-11 { grid-area: d-11; }
.lights-out .boad .d-12 { grid-area: d-12; }
.lights-out .boad .d-13 { grid-area: d-13; }
.lights-out .boad .d-14 { grid-area: d-14; }
.lights-out .boad .d-15 { grid-area: d-15; }

.lights-out fieldset{
	width:50%;
	margin:0 auto;
	padding:1%;
}

.lights-out .btns > div{
	display:flex;
	justify-content:center;
}

.lights-out .btns .button-area{
	transform: translate(0%) scale(0.6);
}

.lights-out #clear-judge{
	visibility:hidden;
}

.lights-out .message{
	width:50%;
	margin:0 auto;
}

.lights-out .message p{
	margin:0;
	font-size:17.5px;
}

.lights-out .message.clear{
	color:red;
	font-weight:bold;
}
 

JavaScript


const square_root = 4;
let dots_number = [];
const dots_reset = [];
let start_count = 0;
let steps;
let random_numbers;

for(let i = 0; i < square_root * square_root; i++){
    dots_number.push(0);
    dots_reset.push(0);
}

const lights_out = document.querySelector(".lights-out");
const bord_dots = lights_out.querySelector(".boad").querySelectorAll("div");
const start_btn = lights_out.querySelector("#start-btn");
const reset_btn = lights_out.querySelector("#reset-btn");
const radio_btns = document.getElementsByName("options");
const message = lights_out.querySelector(".message");
const count_number = message.querySelector("#count-number");
const clear_judge = message.querySelector("#clear-judge");

function Reset(){
    for(let i = 0; i < dots_number.length; i++){
        if(dots_number[i] == 1){
            bord_dots[i].classList.add("red");
        }else if(dots_number[i] == 2){
            bord_dots[i].classList.remove("red");
            bord_dots[i].classList.add("green");
        }else if(dots_number[i] == 0){
            bord_dots[i].classList.remove("red");
            bord_dots[i].classList.remove("green");
        }
    }
}

start_btn.addEventListener("click", function(){
    for(let i = 0; i < radio_btns.length; i++){
        if(radio_btns[i].checked){
            steps = radio_btns[i].defaultValue;
        }
    }

    gameStart();
});

let clear = false;
let count = -steps;

function newGame(){
    clear = false;
    count = -steps;
    clear_judge.style.visibility = "hidden";
    message.classList.remove("clear");

    for(let i = 0; i < random_numbers.length; i++){
        bord_dots[random_numbers[i]].click();
    }
}

reset_btn.addEventListener("click", function(){
    if(bord_dots.length == dots_number.length){
        dots_number = [...dots_reset];
        Reset();
    }

    newGame();
});

function gameStart(){
    if(bord_dots.length == dots_number.length){
        function getRandomNumbers(n) {
            const numbers = Array.from({ length: square_root * square_root }, (_, i) => i);
            const result = [];
            
            for (let i = 0; i < n; i++) {
                const randomIndex = Math.floor(Math.random() * numbers.length);
                result.push(numbers[randomIndex]);
                numbers.splice(randomIndex, 1);
            }
            
            return result;
        }

        function Reversible(n){
            if(dots_number[n] == 0){
                dots_number[n] = 1;
            }else if(dots_number[n] == 1){
                dots_number[n] = 2;
            }else{
                dots_number[n] = 0;
            }
        }

        function judge(){
            let lights = true;

            for(let i = 0; i < dots_number.length; i++){
                if(dots_number[i] > 0){
                    lights = false;
                }
            }

            if(lights){
                clear = true;
                message.classList.add("clear");
                clear_judge.style.visibility = "visible";
            }
        }

        function fourDirections(n){
            Reversible(n);

            if((n / square_root) >= 1){
                Reversible(n - square_root);
            }

            if((n / square_root) < (square_root - 1)){
                Reversible(n + square_root);
            }

            if((n % square_root) > 0){
                Reversible(n - 1);
            }

            if((n % square_root) < (square_root - 1)){
                Reversible(n + 1);
            }

            Reset();
            count++;
            judge();
            count_number.textContent = count;
        }

        dots_number = [...dots_reset];
        Reset();

        if(start_count == 0){
            for(let i = 0; i < bord_dots.length; i++){
                bord_dots[i].addEventListener("click", function(){
                    if(!clear){fourDirections(i)};
                });
            }

            start_count++;
        }

        random_numbers = getRandomNumbers(steps);
        newGame();
    }
} 
 

出典

電気はこまめにきりましょう
というわけで、「ライツアウト」です。1995年に㈱タカラ(当時)さんより販売された携帯型パズル玩具ですね。5×5、計25個のとある法則により点灯・消灯するボタンを使い、全てのボタンのライトを消す(ライツアウト)とゲームクリア、と、まあ、そげな感じのものです。カラすけの青春時代である90年代の玩具ですからね、ええ、当然遊...
みらいワークの施設利用者
チュンチュン

「WordPress」でのHTML・CSS・JavaScriptに関する情報を、
提供させて頂く予定です。
使用プラグインは「Simple Custom CSS and JS」で、
JavaScriptの表示位置は必ず「要素内」です。
ただし、使用環境により異なる事をご了承ください。

サイト制作
「みらいワーク」利用者ノート
タイトルとURLをコピーしました