操作系が安定してきたところで、入力した数値を保存して、ナンプレのゲームを途中で中断したり、ブラウザをリロードしても、ゲームの続きをプレイできるようにしたいと思います。
次のソースコードを上書き、又は新規作成して、環境を更新してください。
ソースコード
[更新] main.js
import { View } from './js/view.js'
import { Input } from './js/input.js'
import { Question } from './js/question.js'
import { Data } from './js/data.js'
import { Common } from './js/common.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.data = new Data()
Main.view = new View()
Main.input = new Input()
Main.question = new Question({
callback : (e => {
Common.continue()
}).bind(this)
})
}
switch(document.readyState){
case 'complete':
case 'interactive':
init()
break
default:
window.addEventListener('DOMContetLoaded' , init)
break
}
[新規] js/data.js
import { Main } from '../main.js'
import { Common } from './common.js'
export class Data{
save(){
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(){
const json = window.localStorage.getItem(Main.save_name)
if(!json){return null}
return JSON.parse(json)
}
}
[新規] 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()
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(cell.getAttribute('data-status') === type){
return Number(cell.textContent || 0)
}
else{
return 0
}
}
}
[更新] js/input.js
import { Main } from '../main.js'
import { Element } from './element.js'
import { Common } from './common.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()
}
// 移動距離を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':
console.log('check')
break
case 'start':
default:
Common.start()
Main.data.save()
break
}
}
}
[更新] 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)
}
}
}
[更新] question.js
import { Main } from '../main.js'
import { Element } from './element.js'
export class Question{
constructor(options){
this.options = options || {}
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)
}
this.finish()
}).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' , '')
}
}
}
}
finish(){
if(this.options.callback){
this.options.callback()
}
}
}
解説とポイント
上記ソースコードで更新したページで文字を入力して、ブラウザのページをリロードしてみましょう。
入力した数値や、問題数値がすぐに現れていれば成功です。
もし、うまく表示出来ない人は、ブラウザキャッシュでモジュールが更新されていない可能性があるので、ブラウザのスーパーリロードをしてみてください。
GoogleChromeであれば、シフトを押しながらリロードボタンをクリックするか、デバッグコンソールを表示した状態で、controlを押しながら、リロードボタンを押して「キャッシュの消去とハード再読み込み」を選択してください。
今回のメインモジュール
今回の注目するモジュールは、data.jsです。
中を見ると、非常に簡単なコードで、saveとloadの処理を書いているだけです。
ただ、saveする時にどんなデータを保存するかというデータ作成は、common.jsに、get_matrix_numbers() という関数を作って対応しています。
localStorageの読み込みと書き込み処理は非常に簡単なので覚えておくとcookieよりも手軽に使えて便利ですよ。
get_matrix_numbers関数について
この関数では、tableタグのセルをfor文で1つずつ見ていって、データ登録した時のような、9x9のマトリクス2重配列データを作っています。
typeという値で、問題の数値と入力した数値の2種類を分類して、それぞれ保存するようにしています。
ちなみに、問題は、データに保存されているので、本来は問題のIDを保存しておけばいいだけなんですが、データの仕様が今後変わる可能性もあるので、必要情報は保存しておくことにしました。
保存タイミング
データを保存するイベントは、数値を入力したタイミングです。
Main.jsで最初にmain.dataという変数にdataクラスのインスタンスを保存して、いろいろな処理を引き継いでいく仕様にしています。
実際の入力処理である、input.jsの中の、72行目あたりに、mouseupのタイミングで、Main.data.save() という関数を実行しています。
これで、入力をするたびに、localStorageにデータを書くようにしています。
すでに書かれてあるデータがあれば、上書きをするという簡易な処理です。
データの読み込み
次に、ページを読み込んだ時(リロードした時)に、localStorageに保存してあるデータがあれば、それを表示するという処理を、Main.jsの、questionデータを読み込んだcallback関数内に書いています。
バケツリレー方式になっていますが、Commonクラスのcontinueという関数にデータをloadして、ゲームをスタートさせるようにしています。
数値をマス目に書き込む処理は、問題数値も、入力数値も、どれも同じ関数で対応できるように、Common.put_number(データ) という関数を作って対応しています。
当たり前ですが、プログラミングは、このように似たような処理は、同じ関数にして使うのが鉄則で、こうしてまとめていくことで、書き込むソースコードが少なくなります。
この辺の感覚は、教科書を見ても覚えられないので、たくさんコードを書いたり、人のコードを見て、自分なりのまとめ方を身に着けていくというのが良いと思います。
0 件のコメント:
コメントを投稿