【プログラムを作ろう】 ナンプレ編 #2

こんにちは!
エルフィールドでエンジニアとして働いている、H.Tです。
ナンバープレイスをテーマにしたプログラム作成シリーズの第2回です。
前回は、HTMLとJavaScriptでナンプレの「枠組み」を作成しました。
今回は、実際に数字を当てはめていき、パズルが完成した状態を目指します!
■ 設計方針
まずは、ナンプレのルールをおさらいしておきましょう。
- 各「行」に 1〜9 の数字を重複なく1つずつ配置する
- 各「列」にも 1〜9 の数字を重複なく1つずつ配置する
- 各「3×3ブロック」にも 1〜9 の数字を重複なく1つずつ配置する
このルールを守るには、数字を配置するたびに以下のような流れをたどる必要があります。
- 今注目しているマスが属する行・列・ブロックに、すでに使われている数字を取得
- 1〜9のリストから、それらの数字を取り除く
- 残った数字からランダムに1つ選んで、そのマスに配置する
- 次のマスに移って同じ処理を繰り返す
では、このロジックを実現する具体的なコードを紹介していきます。
■ 処理の実装ポイント
1. class属性から行・列・ブロック情報を取得
各マスには class="r1 c4 b01"
のような形で、行・列・ブロックの情報を埋め込んでいます。
それを使って、以下のように情報を取り出します。
tmpClasses = tmpTD.getAttribute('class').split(' ')
→ 結果は ['r1', 'c4', 'b01']
のようなリストになります。
2. クラスに属するマスから数字を取得する関数
function getNums(classx){
outlist = new Set()
listTD = document.querySelectorAll('.' + classx)
listTD.forEach(cell => {
if (cell.innerText !== '') {
outlist.add(cell.innerText)
}
})
return outlist
}
querySelectorAll('.' + classx)
で、該当するクラス(行・列・ブロック)に属するすべてのマスを取得- 数字が入っているマスだけ
Set
に追加します(※重複自動排除のため)
3. 数字リストから使えない数字を除く関数
function removeNum(listx, listy){
listy.forEach(y => {
y = Number(y)
i = listx.indexOf(y)
if(i !== -1){
listx.splice(i, 1)
}
})
return listx
}
listx
は 1〜9 の数字リストlisty
はすでに使われている数字- 重複を避けるため、
indexOf
とsplice
を組み合わせて対象数字を削除します
4. ランダムに数字を選ぶ関数
function selectRndNum(listx){
return listx[Math.floor(Math.random() * listx.length)]
}
定番の乱数処理。Math.random()
で 0〜1未満の小数を生成し、Math.floor()
で整数に変換して、配列のインデックスとして使います。
■実際のプログラム
<!DOCTYPE html>
<html>
<head>
<title>Number Place</title>
<style>
#app TABLE{border-collapse: collapse;}
#app TD{width: 3rem; height: 3rem; text-align: center; border: solid 1px #CCC ;}
#app TD:nth-child(1), #app TD:nth-child(3n){border-left-color: #333;}
#app TD:nth-child(3n){border-right-color: #333;}
#app TR:nth-child(1) TD, #app TR:nth-child(3n) TD{border-top-color: #333;}
#app TR:nth-child(3n) TD{border-bottom-color: #333;}
</style>
</head>
<body>
<div id="app"></div>
<script>
list08 = [0,1,2,3,4,5,6,7,8]
function init(){
myTable = document.createElement('table')
list08.forEach(i => {
tmpTR = document.createElement('tr')
list08.forEach(j => {
tmpTD = document.createElement('td')
tmpBlock = 'b' + Math.trunc(i/3) + Math.trunc(j/3)
tmpTD.setAttribute('class', ('r' + i) + ' ' + ('c' + j) + ' ' + tmpBlock)
tmpTR.append(tmpTD)
})
myTable.append(tmpTR)
})
document.querySelector('#app').append(myTable)
}
function getNums(classx){
// classx にある数字を拾って配列で返す。空欄は除く
outlist = new Set()
listTD = document.querySelectorAll('.' + classx)
listTD.forEach(cell => {
if (cell.innerText != ''){
outlist.add(cell.innerText)
}
})
return outlist
}
function removeNum(listx, listy){
// 配列listx から、配列listy にある数字を取り除いて返す
listy.forEach(y => {
y = Number(y)
i = listx.indexOf(y)
if(i != -1){
listx.splice(i, 1)
}
})
return listx
}
function selectRndNum(listx){
// 配列listy から、ランダムにひとつ数字選んで返す
return listx[Math.floor(Math.random() * listx.length)]
}
function putNumber(){
myTable = document.getElementsByTagName('table')[0]
list08.forEach(i => {
tempTR = myTable.getElementsByTagName('tr')[i]
list08.forEach(j => {
tmpTD = tempTR.getElementsByTagName('td')[j]
tmplist19 = [1,2,3,4,5,6,7,8,9]
tmpClasses = tmpTD.getAttribute('class').split(' ')
tmpClasses.map( (classx) => {
tmpNums = getNums(classx)
tmplist19 = removeNum(tmplist19, tmpNums)
})
tmpCellNum = selectRndNum(tmplist19)
tmpTD.append(tmpCellNum)
})
})
}
init()
putNumber()
</script>
</body>
</html>
■ 実行してみた結果
実行すると、左上の方はうまく数字が入っているように見えますが、ところどころundefined
が表示されてしまいます。
たとえば2行目では「3」が使えるはずの数字として残っていたのに、他のブロックで「3」がすでに使われており、
置けなくなってしまった、というパターンです。
これは、単に「その場で置ける数字」を見ているだけでは不十分で、
将来的に置けなくなってしまうことを見越して「今のうちに使っておくべき数字」を考慮する必要があるからです。
■ 気づきと反省
今回のロジックでは、各マスで「現在使えない数字」だけを考慮して進めていましたが、
これでは 全体として一貫性のある完成状態を作るのは難しいことがわかりました。
原因は明確ですが、これを回避するにはロジックの全面的な作り直しになります。
今日はここまでです!
次回は、この課題にどう対処するかを考えながら、ロジックの見直しを行っていきます。
どこで詰まりやすいのか、どうすれば<確実に完成させられる>のか、一緒に模索していきましょう!
それでは、また次回👋