数値入力がうまくできるようになったら、次に問題となる数値の入力をしたいと思います。
問題の数値が表示されて、数値入力ができるようになったら、基本的なゲームの表示とコントロールができるので、ゲームとしての構造が出来上がります。
問題データの作成
ただ、今回のナンプレゲームは、問題を自動で生成する事はしないので、問題データを事前に作っておく必要があります。
9 x 9 のマス目に、どの数値を置くかというデータは、データ容量を削減するために、bitmap方式で書いて、データ圧縮して、軽量データにするという事は可能ですが、
ゲームが完成した後で、ゲームの問題をどんどん追加していくことを考えると、見た目が分かりやすいデータにした方がいいと思い、jsonの2重配列で作る事にしました。
問題データサンプル
[
[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]
]
0の数字は、空欄になる箇所で、添字的に入れています。
こうすることで、データを見ただけで、どのマスに何の数値が入るのかが分かりやすいですからね。
そしてこれを、複数登録できる配列データとして、1つのjsonファイルに格納して使います。
ちなみに、実際のデータファイルは次のようにしました。
[新規] data/questions.json
初回は全部で12問作っておきました。
idは、今の時点では使わないのでなくても良いのですが、こういうデータを作ったときは、id値を入れておくと後々に助かることが多いので、おまじないのように入れておきました。
[
{
"id" : 1,
"data" : [
[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]
]
},
{
"id" : 2,
"data" : [
[0,0,1, 0,0,0, 3,0,0],
[0,0,0, 9,0,1, 0,0,4],
[2,3,0, 0,0,7, 0,6,9],
[0,0,8, 0,6,0, 0,7,1],
[0,0,0, 0,0,0, 0,0,0],
[1,9,0, 0,0,0, 0,0,0],
[0,0,0, 8,0,9, 0,0,6],
[0,0,0, 3,0,0, 0,4,0],
[4,2,0, 0,0,0, 0,0,0]
]
},
{
"id" : 3,
"data" : [
[0,0,2, 0,0,0, 6,0,0],
[0,0,0, 3,0,8, 9,5,0],
[7,0,8, 0,0,5, 0,0,0],
[0,0,0, 0,5,6, 0,0,8],
[0,7,0, 0,9,0, 0,2,0],
[0,8,0, 0,0,0, 5,0,0],
[0,0,0, 9,2,0, 0,0,0],
[5,0,3, 0,0,0, 0,0,0],
[0,0,0, 0,4,3, 1,0,0]
]
},
{
"id" : 4,
"data" : [
[0,0,4, 0,0,0, 3,0,0],
[0,9,0, 0,0,8, 6,7,0],
[1,2,0, 0,0,9, 0,0,8],
[0,0,0, 0,0,0, 7,2,5],
[0,0,0, 0,0,0, 0,0,4],
[9,0,0, 0,2,0, 0,0,0],
[0,0,0, 5,0,0, 0,0,7],
[0,0,0, 0,6,0, 9,4,0],
[2,0,8, 0,1,0, 0,3,0]
]
},
{
"id" : 5,
"data" : [
[0,0,2, 0,0,0, 5,0,0],
[0,8,6, 0,0,0, 0,0,0],
[0,0,0, 5,0,0, 7,1,0],
[8,9,0, 0,5,0, 0,0,2],
[0,0,4, 0,8,0, 0,3,0],
[2,0,0, 0,0,0, 0,0,0],
[4,0,0, 0,0,2, 0,0,0],
[7,0,0, 0,4,0, 0,8,0],
[0,0,9, 0,0,8, 2,6,0]
]
},
{
"id" : 6,
"data" : [
[0,0,7, 0,0,0, 3,0,0],
[8,0,0, 0,0,0, 0,4,9],
[5,6,0, 0,0,0, 0,0,1],
[2,0,3, 0,9,0, 0,0,0],
[0,7,0, 0,0,3, 0,0,6],
[0,0,5, 0,0,1, 0,0,0],
[0,8,0, 1,0,0, 5,0,0],
[0,0,4, 0,0,0, 0,9,0],
[7,1,0, 2,0,0, 0,0,0]
]
},
{
"id" : 7,
"data" : [
[0,0,9, 0,0,0, 6,0,0],
[4,0,0, 2,0,0, 9,0,0],
[0,0,0, 0,0,0, 0,0,0],
[0,7,5, 4,0,0, 0,0,0],
[0,0,0, 9,0,2, 0,8,0],
[6,0,0, 0,0,0, 0,2,5],
[0,4,0, 0,0,0, 1,9,7],
[9,0,0, 0,3,0, 0,0,0],
[0,1,0, 0,8,0, 0,0,0]
]
},
{
"id" : 8,
"data" : [
[0,0,6, 0,0,0, 1,0,0],
[4,0,0, 1,0,8, 0,5,9],
[7,0,0, 2,0,0, 0,0,0],
[0,5,0, 9,0,0, 4,0,0],
[0,0,0, 0,0,6, 3,9,1],
[0,6,7, 0,4,0, 2,0,0],
[0,0,0, 0,7,0, 0,0,8],
[1,0,0, 0,0,0, 0,7,0],
[0,0,0, 4,0,0, 0,0,0]
]
},
{
"id" : 9,
"data" : [
[0,0,6, 0,0,0, 5,0,0],
[3,0,1, 0,8,0, 0,6,9],
[9,0,0, 0,0,5, 0,1,0],
[5,0,0, 6,0,0, 8,0,0],
[0,0,8, 0,0,0, 1,0,0],
[0,6,0, 8,7,0, 0,0,0],
[0,0,0, 0,0,1, 9,5,3],
[0,0,0, 4,9,6, 0,0,0],
[0,1,0, 0,0,0, 0,0,2]
]
},
{
"id" : 10,
"data" : [
[0,0,9, 0,0,0, 6,0,0],
[6,1,0, 0,0,0, 0,2,0],
[0,5,0, 0,3,0, 1,0,0],
[0,7,0, 5,0,8, 2,6,0],
[0,6,2, 0,0,4, 0,0,0],
[0,0,0, 0,0,1, 4,0,5],
[2,0,0, 0,6,0, 0,5,0],
[1,8,0, 0,0,0, 0,9,0],
[9,0,0, 0,0,5, 0,0,0]
]
},
{
"id" : 11,
"data" : [
[6,0,0, 4,0,0, 0,0,0],
[5,0,0, 0,0,7, 0,9,0],
[0,1,0, 0,8,0, 3,0,0],
[0,0,2, 7,0,0, 0,0,4],
[0,5,0, 0,0,0, 0,6,0],
[1,0,0, 0,0,9, 5,0,0],
[0,0,9, 0,7,0, 0,4,0],
[0,8,0, 3,0,0, 0,0,2],
[0,0,0, 0,0,6, 0,0,3]
]
},
{
"id" : 12,
"data" : [
[2,0,1, 0,0,0, 9,0,7],
[0,0,0, 1,0,4, 0,0,0],
[0,0,8, 0,3,0, 2,0,0],
[3,0,0, 0,0,0, 0,0,1],
[0,5,0, 0,7,0, 0,3,0],
[9,0,0, 0,0,0, 0,0,8],
[0,0,9, 0,1,0, 6,0,0],
[0,0,0, 2,0,5, 0,0,0],
[4,0,7, 0,0,0, 3,0,2]
]
}
]
問題表示処理
次に問題を表示する処理のクラスファイルを作りました。
これまでのモジュールの置き換えと、新しいモジュールの追加をして、環境を更新してください。
[新規] js/question.js
import { Main } from '../main.js'
import { Element } from './element.js'
export class Question{
constructor(){
this.load()
}
load(){
const xhr = new XMLHttpRequest()
xhr.open('get' , Main.data_path , true)
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = (e => {
if(xhr.readyState !== XMLHttpRequest.DONE){return}
const status = xhr.status;
if (status === 0
|| (status >= 200 && status < 400)
|| e.target.response) {
this.datas = JSON.parse(e.target.response)
}
}).bind(this)
xhr.send()
}
new(num){
Main.question_num = num || Main.question_num
const data = this.datas[Main.question_num]
if(!data || !data.data){return}
this.put_numbers(data.data)
}
put_numbers(datas){
const tr_lists = Element.tr_lists
if(!tr_lists || !tr_lists.length){return}
for(let i=0; i<datas.length; i++){
const td_lists = tr_lists[i].getElementsByTagName('td')
for(let j=0; j<datas[i].length; j++){
if(datas[i][j]){
td_lists[j].textContent = datas[i][j]
td_lists[j].setAttribute('data-status' , 'lock')
}
else{
td_lists[j].textContent = ''
td_lists[j].setAttribute('data-status' , '')
}
}
}
}
}
[新規] element.js
import { Main } from '../main.js'
export class Element{
static get table(){
return document.getElementById(Main.stage_id)
}
static get tr_lists(){
return this.table.getElementsByTagName('tr')
}
static get elm_button(){
return document.querySelector('button#btn')
}
}
[更新] main.js
import { View } from './js/view.js'
import { Input } from './js/input.js'
import { Question } from './js/question.js'
export const Main = {
stage_id : 'NumberPlace',
data_path : 'data/questions.json',
save_name : 'mynt_number_place_save',
clear_name : 'mynt_number_place_clear',
interval_px : 10,
question_num : 0,
}
function init(){
Main.view = new View()
Main.input = new Input()
Main.question = new Question()
}
switch(document.readyState){
case 'complete':
case 'interactive':
init()
break
default:
window.addEventListener('DOMContetLoaded' , init)
break
}
[更新] js/input.js
import { Main } from '../main.js'
import { Element } from './element.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
}
// 移動距離を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':
break
case 'start':
default:
Element.elm_button.setAttribute('data-status' , 'check')
Main.question.new(Main.question_num)
break
}
}
}
[更新] index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>NumberPlace</title>
<link type='text/css' rel='stylesheet' href='style.css' />
<script type='module' src='main.js'></script>
</head>
<body>
<h1>NumberPlace</h1>
<div id='NumberPlace'><button id='btn' data-status='start'></button></div>
</body>
</html>
[更新] style.css
@import 'css/common.css';
@import 'css/table.css';
@import 'css/button.css';
[新規] 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='disabled']{
pointer-events:none;
background-color:#bbb;
color:#ddd;
}
[新規] css/common.css
#NumberPlace{
--size-stage : 300px;
--color-border-1 : black;
--color-border-2 : #666;
}
@media (max-width:500px){
#NumberPlace{
--size-stage : calc(100vw - 2px);
}
}
html,body{
width : 100%;
height : 100%;
padding : 0;
margin : 0;
border : 0;
outline : 0;
scroll-behavior : smooth;
}
body{
border : 1px solid transparent;
}
#NumberPlace,
#NumberPlace *,
#NumberPlace *::before,
#NumberPlace *::after{
-webkit-box-sizing : border-box;
-moz-box-sizing : border-box;
-o-box-sizing : border-box;
-ms-box-sizing : border-box;
box-sizing : border-box;
-webkit-font-smoothing: antialiased;
font-family : base-font , sans-serif;
}
table{
border-collapse:collapse;
}
.no-select{
-webkit-user-select : none;
-moz-user-select : none;
-ms-user-select : none;
user-select : none;
}
ul,ol,li{
list-style : none;
margin : 0;
padding : 0;
}
.hidden{
display : none!important;
}
button{
background : none;
border : none;
outline : none;
-webkit-appearance : none;
-moz-appearance : none;
appearance : none;
}
.flex{
display : flex;
}
input[type='text'],
input[type='number']{
outline : 0;
}
.right{
text-align : right;
}
[新規] css/table.css
table{
border-collapse:collapse;
user-select: none;
}
td{
padding:0;
margin:0;
}
#NumberPlace td{
width : calc(var(--size-stage) / 9);
height : calc(var(--size-stage) / 9);
border : 1px solid var(--color-border-2);
cursor : pointer;
text-align : center;
vertical-align : middle;
background-color : white;
font-size : 20px;
font-weight : bold;
}
#NumberPlace tr:nth-child(1){
border-top : 2px solid #000;
}
#NumberPlace tr:nth-child(3n){
border-bottom : 2px solid #000;
}
#NumberPlace td:nth-child(1){
border-left : 2px solid #000;
}
#NumberPlace td:nth-child(3n){
border-right : 2px solid #000;
}
#NumberPlace td[data-status='lock']{
color:#66F;
}
@media (hover:hover){
#NumberPlace td:hover{
background-color : #DDD;
}
}
@media (hover:none){
}
解説とポイント
1. main.jsでの初期処理
ゲームのコントロール部分のmain.jsでは、今回新しく作ったquestion.jsを読み込んで、クラスを実行しています。
2. question.jsのインスタンス起動処理(constructor)
その流れで、question.jsのクラスQuestionでは、最初に、data/question.jsonをajax処理で読み込む load処理が実行されます。
読み込みが正常に完了すると、this.datasにデータが格納されて、以後はクラス内のdatas変数から参照できるようになります。
3. input.jsにstartボタンイベント登録
イベント処理を追加して、startボタンを押した時に、question.jsのnew関数を実行して、新規に問題を表示する処理に進みます。
4. question.jsのnew関数
今現在の問題がデータに登録してある配列の何番目かを格納しているのが、Main.question_numという変数です。
指定された問題番号または、事前に格納してある番号を元にajaxで読み込んだデータの中から問題データのみを抽出します。
それを配列に沿って、テーブルタグの中のセルを書き換えていく処理をしています。
ただし、0の時は、何も書き込まない(ブランクを書き込む)処理をすれば、問題の表示が完了になります。
注意点として、問題で数字が書き込まれるセルに、data-status = 'locked'という属性を付けています。
これは、問題の数字は、変更が出来ないようにする数値ロック機能にしていて、前回作った、input.jsでの数値の書き換え処理を防止するif文を入れています。
あとがき
この時点で、ナンプレのゲーム自体は遊べる状態になっていますが、実際に数字を置いていってそれが正解なのか、間違っているのか、判定する必要があります。
その判定処理を次回やろうと思ったのですが、その前に、配置したデータをブラウザをリロードしても残しておくために、次回はtableの状態をセーブする処理を追加してみたいと思います。
0 件のコメント:
コメントを投稿