もう20年も前の事になりますが、子供の頃、
現在のタカラトミーが2000年に発売したライツアウト※に熱中していました。
これは、どういうゲームかと言うと、一つのマス目を押すと、
そのマス目と共に、上下左右のマスのランプが消⇔点へと反転するものです。
例えば4×4マスであれば、全て点灯している状態から、
全て反転させるのは容易ですが、
それが5×5になると、一気に難易度が跳ね上がります。
ただ、言葉で説明するのは難しいので、ホームページに組み込める形で、
4×4のライツアウトを参考にパズルをJavaScriptで作りました。
「ライツアウト」は恐らく商標だと思うので、
本家様と区別する為に「点灯パズル」という表現を使わせて頂きます。
4×4パズル
点灯パズルの難易度
このパズルは消→赤→緑と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();
}
}
出典
※
