[ブラウザゲームの作り方] Number-Place(数独)#7「エラー表示」

2023年5月26日

ゲーム プログラミング

eyecatch ナンプレゲームで正解と不正解の判定ができるようになったので、今回は、不正解の時に、どこの箇所がエラーなのかをわかりやすく表示する処理を追加します。 ゲームとしては、この処理を追加しないと、どこが間違っているかわからなくて、非常に難しいゲーム難易度になってしまうため、 数独ゲームでは大体の場合、この機能が備わっていますね。

ソースコード

[更新] css/table.css

※次のコードを追加 #NumberPlace td[data-error='1']{ background-color:#FDD; }

[更新] js/check.js

※ファイル内を総入れ替え import { Main } from '../main.js' import { Element } from './element.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 = this.get_over(arr) return res.length ? true : false } get_over(arr){ return arr.filter((a,b,c)=>{return c.indexOf(a) === b && b !== c.lastIndexOf(a)}) } // 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 } convert_pivot_datas_reverse(datas){ const pivot_datas = [] for(let i=0; i<datas.length; i++){ for(let j=datas[i].length-1; j>=0; 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 } get_error_matrix(){ const error_datas = [] // vertical-1 const vertical_datas = this.convert_pivot_datas(this.datas) const vertical_datas_new = [] for(const pivot_data of vertical_datas){ const data = pivot_data.map((num , i , arr) => {return arr.indexOf(num) !== arr.lastIndexOf(num) ? 1 : 0}) vertical_datas_new.push(data) } const vertical_datas_reverse = this.convert_pivot_datas_reverse(vertical_datas_new) // cube-1 const cube_datas = this.convert_cube_datas(this.datas) const cube_datas_new = [] for(const cube_data of cube_datas){ const data = cube_data.map((num , i , arr) => {return arr.indexOf(num) !== arr.lastIndexOf(num) ? 1 : 0}) cube_datas_new.push(data) } const cube_datas_reverse = this.convert_cube_datas(cube_datas_new) for(let i=0; i<this.datas.length; i++){ error_datas[i] = new Array(9).fill(0) // empty const empties = this.datas[i].map((num , i , arr) => {return !num ? 1 : 0}) error_datas[i] = this.error_overwrite(error_datas[i] , empties) // horizon const horizon = this.datas[i].map((num , i , arr) => {return arr.indexOf(num) !== arr.lastIndexOf(num) ? 1 : 0}) error_datas[i] = this.error_overwrite(error_datas[i] , horizon) // vertical-2 error_datas[i] = this.error_overwrite(error_datas[i] , vertical_datas_reverse[i]) // cube-2 error_datas[i] = this.error_overwrite(error_datas[i] , cube_datas_reverse[i]) } return error_datas } error_overwrite(array_base , array_overwrite){ for(let i=0; i<array_overwrite.length; i++){ if(array_base[i]){continue} if(!array_overwrite[i]){continue} array_base[i] = array_overwrite[i] } return array_base } fail(){ const error_datas = this.get_error_matrix() console.log(error_datas) Main.view.error(error_datas) } correct(){ Main.data.save_clear() Main.view.correct() } }

[更新] 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() datas.push({ question_num : Main.question_num, input : Common.get_matrix_numbers(''), question : Common.get_matrix_numbers('lock'), }) 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/input.js

※86行目あたりを次のように書き直す new Check() ↓ Main.check = new Check()

[更新] js/view.js

※error関数を次のように書き換える // error error(error_datas){ alert('違うよ') const tr_lists = Element.tr_lists 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++){ if(error_datas[i][j]){ td_lists[j].setAttribute('data-error' , 1) } else if(td_lists[j].hasAttribute('data-error')){ td_lists[j].removeAttribute('data-error') } } } }

確認方法

ナンプレゲームの1問目のlocalStorageセーブデータを書き換えてみましょう。 これまでセーブデータが書き込まれていると思うので、GoogleChromeの「ファイル」-「新規シークレット・ウィンドウ」を選択して、表示された画面で「Start」ボタンを押した状態で以下の作業を行って下さい。

1. GoogleChromeのデバッグコンソールを開いて「Application」タブをクリックする

2. mynt_number_place_saveというkeyのvalue箇所をダブルクリックする

3. 下のコードをそのまま貼り付ける。

{"question_num":0,"input":[[6,9,0,8,1,5,0,3,4],[8,5,2,0,3,0,1,9,7],[0,3,1,9,7,0,5,6,0],[3,7,9,2,4,0,6,8,0],[0,4,8,7,0,6,3,0,9],[5,1,0,3,0,9,4,0,2],[9,0,5,1,2,3,7,4,6],[0,6,4,0,0,7,0,2,0],[0,0,3,0,6,8,9,5,1]],"question":[[0,0,7,0,0,0,2,0,0],[0,0,0,6,0,4,0,0,0],[4,0,0,0,0,2,0,0,8],[0,0,0,0,0,1,0,0,5],[2,0,0,0,5,0,0,1,0],[0,0,6,0,8,0,0,7,0],[0,8,0,0,0,0,0,0,0],[1,0,0,5,9,0,8,0,3],[7,2,0,4,0,0,0,0,0]]}

4. ページを再読み込みすると1問目の正解が書き込まれている。

5. どこかの数値を書き換えて間違えた状態にする。

※右上の数値を4→5に変更してみました。

6. 「Check」ボタンをクリックして、エラー表示を確認。

上記のように、エラーの箇所(数値が重複している箇所)に色がついてマーキングされれば成功です。

解説とポイント

今回の基本的にやったことは、 エラーの箇所(tableのtdセル)に、エラーの場合は、data-error="1"という属性をセットするという処理を追加しました。 cssでは、#NumberPlace td[data-error='1']というセレクタを追加しただけなので、分かりやすいと思います。 javascript箇所は、check.jsで正誤判定をして、誤判定した場合に、fail()という関数を実行して、get_error_matrix ()関数で、間違った座標を取得しています。 次にview.jsのerror()関数で、tdタグに属性をセットしているという流れです。 あまり複雑な処理はしていないので、プログラムを見てもらうと流れが理解できると思います。 get_error_matrix()関数では、あまり効率的な書き方はしていないので、もっと効率的に書けるといいかもしれませんね。

ゲームのエラーについてのポイント

今回書き換えた値のところだけをマーキングするというのは、難易度が高いので、正誤判定で重複した数値箇所全部にマーキングをするようにしています。 実は、このやり方では、重複箇所の取得をしているだけなので、正確に間違っている箇所を取得しているのではないんですね。 ナンプレゲームでは、間違って数値を書き込んでも、重複さえしていなければ、エラー表示されないので、エラー箇所があっても、全てのデータを書き直す事もよくやる作業です。 エラー箇所のみを書き出すという場合は、正解算出をして、その数値と合っていない箇所を色変更するという方法も考えられるのですが、正解箇所を計算で求めるのは非常に難しいため、今回は行わないようにしているので、この点追求してみたい人は是非トライしてみてください。