[ブラウザゲームの作り方] Number-Place(数独)#6「正誤判定処理」

2023年5月25日

ゲーム プログラミング

t f B! P L
ナンプレが遊べるようになってきて、いよいよ正誤判定処理をやるタイミングがやってきました。 このゲームの最も核となる部分ですね。 この正誤判定処理が正確でなかったら、ゲームが面白くもなんともなくなりますから。

事前検討事項

ナンプレゲームにおける、正誤判定のルールをまとめてみました。
【ゲーム正誤判定条件】 1. 全てのマスが埋まっている状態 2. 9マスの縦一列に、1から9の数値が全て入っている 3. 9マスの横一行に、1から9の数値が全て入っている 4. 3x3毎のブロックにそれぞれ、1から9の数値が全て入っている
上記の判定が全てクリアしていれば、ナンプレの問題が正解していると判定していい状態と考えていいでしょう。

ソースコード

[更新] css/button.css

button#btn{ width : calc(var(--size-stage) + 2px); padding:10px; border:2px solid var(--color-border-2); display:block; margin-bottom:5px; background-color:#ddd; border-radius:4px; cursor:pointer; } button#btn:hover{ background-color:#eee; } button#btn:active{ background-color:#fee; border-color:#FAA; color:#FAA; } button#btn[data-status='start']::before{ content : 'Start'; } button#btn[data-status='check']::before{ content : 'Check'; } button#btn[data-status='next']::before{ content : 'Next'; } button#btn[data-status='disabled']{ pointer-events:none; background-color:#bbb; color:#ddd; }

[更新] js/input.js

import { Main } from '../main.js' import { Element } from './element.js' import { Common } from './common.js' import { Check } from './check.js' export class Input{ constructor(){ this.set_event() } get next_num(){ if(!this.data){return null} const next_num = this.data.num + 1 return next_num > 9 ? 0 : next_num } set_event(){ const btn = Element.elm_button if(btn){ btn.addEventListener('click' , this.click_btn.bind(this)) } if(typeof window.ontouchstart !== 'undefined'){ Element.table.addEventListener('touchstart' , this.touchstart.bind(this)) Element.table.addEventListener('touchmove' , this.touchmove.bind(this)) Element.table.addEventListener('touchend' , this.mouseup.bind(this)) } else{ Element.table.addEventListener('mousedown' , this.mousedown.bind(this)) Element.table.addEventListener('mousemove' , this.mousemove.bind(this)) Element.table.addEventListener('mouseup' , this.mouseup.bind(this)) } } touchstart(e){ this.mousedown(e.touches[0]) } touchmove(e){ e.preventDefault() this.mousemove(e.touches[0]) } mousedown(e){ const cell = e.target.closest('td') if(!cell){return} if(cell.getAttribute('data-status') === 'lock'){return} this.data = { cell : cell, num : Number(cell.textContent || 0), pos : { x : e.pageX, y : e.pageY, }, } } mousemove(e){ if(!this.data){return} const size = Math.abs(e.pageX - this.data.pos.x) if(size < Main.interval_px){return} const num = this.pos2num(size) this.data.cell.textContent = num || '' this.data.num = num this.data.move_flg = true } mouseup(e){ if(!this.data){return} if(!this.data.move_flg){ this.data.num = this.next_num } this.data.cell.textContent = this.data.num || '' delete this.data Main.data.save_cache() } // 移動距離を0~9の数値に変換する pos2num(pos){ const num = ~~(pos / Main.interval_px) return num > 9 ? num % 10 : num } click_btn(){ const status = Element.elm_button.getAttribute('data-status') switch(status){ case 'check': new Check() break case 'next': Main.question_num++ Common.start() Main.data.save_cache() break; case 'start': default: Common.start() Main.data.save_cache() break } } }

[更新] js/data.js

import { Main } from '../main.js' import { Common } from './common.js' export class Data{ save_cache(){ if(!Common.is_started){return} const data = { question_num : Main.question_num, input : Common.get_matrix_numbers(''), question : Common.get_matrix_numbers('lock'), } const json = JSON.stringify(data) window.localStorage.setItem(Main.save_name , json) } load_cache(){ const json = window.localStorage.getItem(Main.save_name) if(!json){return null} return JSON.parse(json) } del_cache(){ window.localStorage.removeItem(Main.save_name) } save_clear(){ if(!Common.is_started){return} const datas = this.load_clear() const save_data = { question_num : Main.question_num, input : Common.get_matrix_numbers(''), question : Common.get_matrix_numbers('lock'), } const index = datas.findIndex(e => e.question_num === Main.question_num) // update if(index === null){ datas.push(save_data) } // new else{ datas[index] = save_data } const json = JSON.stringify(datas) window.localStorage.setItem(Main.clear_name , json) } load_clear(){ const json = window.localStorage.getItem(Main.clear_name) return json ? JSON.parse(json) : [] } del_clear(){ window.localStorage.removeItem(Main.clear_name) } }

[更新] js/common.js

import { Main } from '../main.js' import { Element } from './element.js' export class Common{ static get is_started(){ if(Element.elm_button.getAttribute('data-status') === 'check'){ return true } else{ return false } } static start(){ Element.elm_button.setAttribute('data-status' , 'check') Main.question.new(Main.question_num) } static continue(){ const datas = Main.data.load_cache() if(!datas){return} Main.question_num = datas.question_num this.start() this.put_number(datas.input) } static put_number(datas){ const tr_lists = Element.tr_lists if(!tr_lists || !tr_lists.length){return} for(let i=0; i<tr_lists.length; i++){ const td_lists = tr_lists[i].getElementsByTagName('td') for(let j=0; j<td_lists.length; j++){ const num = datas[i][j] if(!num){continue} td_lists[j].textContent = num } } } // type : lock or '' static get_matrix_numbers(type=''){ const tr_lists = Element.tr_lists if(!tr_lists || !tr_lists.length){return} const numbers = [] for(let i=0; i<tr_lists.length; i++){ numbers[i] = [] const td_lists = tr_lists[i].getElementsByTagName('td') for(let j=0; j<td_lists.length; j++){ numbers[i][j] = this.get_cell_status(td_lists[j] , type) } } return numbers } static get_cell_status(cell , type){ if(type === 'all' || cell.getAttribute('data-status') === type){ return Number(cell.textContent || 0) } else{ return 0 } } }

[更新] js/view.js

import { Main } from '../main.js' import { Element } from './element.js' export class View{ constructor(){ if(!Element.table){return} this.set_stage() } set_stage(){ const table = document.createElement('table') Element.table.appendChild(table) this.set_row(table) } set_row(parent){ for(let i=0; i<9; i++){ const row = document.createElement('tr') parent.appendChild(row) this.set_cell(row) } } set_cell(parent){ for(let i=0; i<9; i++){ const cell = document.createElement('td') parent.appendChild(cell) } } error(){ alert('違うよ') } correct(){ alert('正解') Element.elm_button.setAttribute('data-status' , 'next') } }

[新規] js/check.js

import { Main } from '../main.js' import { Common } from './common.js' export class Check{ constructor(){ this.datas = Common.get_matrix_numbers('all') this.judgement() } judgement(){ if(this.check_empty() || this.check_error_horizon() || this.check_error_vertical() || this.check_error_cube()){ this.fail() } else{ this.correct() } } // 空欄がある場合にtrueを返す check_empty(){ for(const data of this.datas){ const empty_lists = data.filter(e => !e) if(empty_lists.length){ this.status = 'empty' return true } } } // 横一列に重複数値がある場合はtrueを返す check_error_horizon(){ for(const data of this.datas){ if(this.check_over(data)){ this.status = 'horizon' return true } } } // 縦一列に重複数値がある場合はtrueを返す check_error_vertical(){ const pivot_datas = this.convert_pivot_datas(this.datas) for(const data of pivot_datas){ if(this.check_over(data)){ this.status = 'vertical' return true } } } // 3x3の枠内に重複数値がある場合はtrueを返す check_error_cube(){ const cube_datas = this.convert_cube_datas(this.datas) for(const data of cube_datas){ if(this.check_over(data)){ this.status = 'cube' return true } } } // 配列内の重複確認 check_over(arr){ const res = arr.filter((a,b,c)=>{return c.indexOf(a) === b && b !== c.lastIndexOf(a)}) return res.length ? true : false } // 2重配列の縦横を入れ替える convert_pivot_datas(datas){ const pivot_datas = [] for(let i=0; i<datas.length; i++){ for(let j=0; j<datas[i].length; j++){ pivot_datas[j] = pivot_datas[j] || [] pivot_datas[j].push(datas[i][j]) } } return pivot_datas } // 3x3毎のブロックで配列を組み直す convert_cube_datas(datas){ const cube_datas = [] let x,y,num for(let i=0; i<datas.length; i++){ x = ~~(i/3) for(let j=0; j<datas[i].length; j++){ y = ~~(j/3) num = x * 3 + y cube_datas[num] = cube_datas[num] || [] cube_datas[num].push(datas[i][j]) } } return cube_datas } fail(){ Main.view.error() } correct(){ Main.data.save_clear() Main.view.correct() } }

解説とポイント

「Check」ボタンを押した時に、判定ルールに沿って処理をして、間違っていれば、「ちがうよ」アラートを表示します。 合っている場合は、「正解」アラートを表示して、ボタンの表示を「Next」に切り替えます。 そして、nextボタンを押すと、次の問題がスタートするようにしています。 肝心の判定処理について、詳しく見てみましょう。

チェック全体フロー

check.jsの、judgement関数に、上記の正誤判定ルールに沿った関数への流れが書かれています。
this.check_empty() : 全てのマスが埋まっているか判定 this.check_error_horizon() : 横一行の入力判定 this.check_error_vertical() : 縦一列の入力判定 this.check_error_cube() : 3x3の中の入力鑑定
上記のどれかが、正解にならなければ、this.fail()に遷移し、 全て正解であれば、this.correct()に遷移します。

全てのマスが埋まっている判定

全てのマスをfor分でチェックして、ブランクになっているマスを確認しています。 ちなみに、マスの値は、this.datasというクラス内の共通変数に、Common.get_matrix_numbers('all')を使って、値を2重変数で格納しています。 これはシンプルで分かりやすいですね。

横一行の入力判定

this.datasの1次配列に、数値が1から9の値が全て入っているかチェックしています。 this.check_over()という関数で、非常に短い判定文が書かれています。 これは、9個の数字でカブりがあるかどうかチェックしています。 const res = arr.filter((a,b,c)=>{return c.indexOf(a) === b && b !== c.lastIndexOf(a)}) return res.length ? true : false javascriptの配列処理は、色々とパワフルで便利なものが多いので、覚えておくといいでしょう。 上記は、配列内の値で重複するものがあれば、resに値を入れるというスニペットで、値が入っていればtrue、値がなければ、重複無しという事でfalseを返しています。 これを、9行分実行すれば、横の行での重複は無いという事が確定します。

縦一列の入力判定

横判定をそのまま応用した処理にしていますが、まず、9x9のマス目を90度回転させる、ピポット処理を行っています。 this.convert_cube_datas(this.datas) この関数に送られたデータは、次のような並びに変更されます。 【ピポットサンプル】 [1,2,3] [4,5,6] [7,8,9] ↓ 次のようにコンバート [3,6,9] [2,5,8] [1,4,7] このデータを横1列判定と同じ判定をすれば、縦の数値がそれぞれ、重複しているか確認できます。

3x3の中の入力鑑定

最後に、3x3のブロック毎の判定をするには、convert_cube_datas()関数を使って、3x3にある9個の数値を取り出して、 その取り出したブロックの中で数値の重複を確認する、先程までと同じ、this.check_over()で確認しています。 convert_cube_datas()関数の解説をしたほうがいいですね。 少し特殊なfor文になっているのは、9x9の全部で81個あるマスを、3x3の全部で9ブロックに分ける処理を行っています。 【ブロック分け処理】 1,1,1, 2,2,2, 3,3,3 1,1,1, 2,2,2, 3,3,3 1,1,1, 2,2,2, 3,3,3 4,4,4, 5,5,5, 6,6,6 4,4,4, 5,5,5, 6,6,6 4,4,4, 5,5,5, 6,6,6 7,7,7, 8,8,8, 9,9,9 7,7,7, 8,8,8, 9,9,9 7,7,7, 8,8,8, 9,9,9 上記のようなkeyをセットして、それを連想配列(cube_datas)のkeyとして、値をいれます。 すると、cube_datas[1]には、3x3の9つの数値が入った配列ができあがり、それが1~9のkeyで構成できます。 あとは、縦横判定処理と同じになります。

ゲームの判定はアルゴリズムが命

今回紹介した判定方法は、できるだけわかりやすく、且つ、できるだけ共通処理になるように書いてみました。 でも、もっとスマートに、短い処理で書くこともできますが、これ以上処理を短くすると、他の人が見てわかりにくくなってしまいそうなので、 今回はこのレベルで行いました。 もし、もっと簡易な方法を思いついたら是非自分のコードで書いて判定をチェックしてみるといいでしょう。 それから、もしナンプレ判定で、今回の方法に抜け漏れなどあれば、こっそり教えてくだされ!

このブログを検索

プロフィール

自分の写真
町田市, 東京都, Japan
プログラミングとサーバーを心の底から楽しむクリエーターです。 経営者であり、開発者でもありますが、得意としているのは、アイデア創出。

ブログ アーカイブ

QooQ