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

こんにちは!
エルフィールドでエンジニアとして働いている、H.Tと申します
今回は「ナンバープレイス・プログラム」の第5回目になります。
前回までで、矛盾のない正解表(81マス)を実用的な確率で生成できるようになりました。
今回はそれをさらに発展させ、正解チェックを自動化して、成功した場合のみ表示されるように改善します。
🔧 設計
正解かどうかを判定するには、各行・各列・各3×3ブロックに含まれる数字が、すべて1〜9のユニークな値であることを
チェックする必要があります。
HTMLでは、各マスに設定したclass属性(行: .r0〜.r8、列: .c0〜.c8、ブロック: .b00〜.b22)を使って、
9個ずつマスを簡単に取得できます。
取得した配列を昇順に並び替えて [1,2,3,4,5,6,7,8,9] と一致するかどうかでチェックします。
この判定をすべて通過するまで、盤面の生成処理をwhileループで繰り返すことで、確実に正しい盤面を表示できるようにします。
🔍 チェック関数の抜粋
以下は、ある行・列・ブロックに含まれるマスの値が 1〜9 のユニークな数字で構成されているかを確認する関数です。
function checkTD(chkclass){
let tmpflag = 0
let checklist = []
let listTD = document.querySelectorAll(chkclass)
listTD.forEach(cell => {
checklist.push(Number(cell.innerText))
})
checklist.sort()
if(checklist.toString() != list19.toString()){
tmpflag = 1
}
return tmpflag
}
ここでは、.querySelectorAll()で対象のマスをすべて取得し、.innerTextで数字を配列に格納→.sort()で昇順に並べ→toString()で比較しています。
JavaScriptでは配列同士を直接比較できないため、一度文字列に変換して比較している点がポイントです。
🔁 メインループのロジック
do {
init()
putNumber()
checkflag = checkComplete()
if(checkflag == 1){
document.getElementsByTagName(‘table’)[0].remove()
}
} while (checkflag)
- init() … 表を初期化(空のマス作成)
- putNumber() … 数字を配置
- checkComplete() … 1~9 が揃っているかチェック
もし正しくなければ、表を .remove() で削除して最初からやり直します。
このループにより、成功した盤面だけが表示されるようになります。
💻 プログラム全体
コードは以下のようになっています。
<!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>
const list02 = [0,1,2]
const list08 = [0,1,2,3,4,5,6,7,8]
const list19 = [1,2,3,4,5,6,7,8,9]
function init(){
myTable = document.createElement(‘table’)
list08.forEach(i => {
let tmpTR = document.createElement(‘tr’)
list08.forEach(j => {
let tmpTD = document.createElement(‘td’)
let tmpBlock = ‘b’ + Math.trunc(i/3) + Math.trunc(j/3)
tmpTD.setAttribute(‘class’, (‘r’ + i) + ‘ ‘ + (‘c’ + j) + ‘ ‘ + tmpBlock)
tmpTD.setAttribute(‘done’, 0)
list19.forEach(n => {
tmpTD.setAttribute(‘is’ + n, 0)
})
tmpTR.append(tmpTD)
})
myTable.append(tmpTR)
})
document.querySelector(‘#app’).append(myTable)
}
function getRndlist19(){
return Array.from(list19).sort((a, b) => Math.floor(Math.random() * 2) * 2 -1)
}
function setDisAbleCell(TD){
// 他のマスで数字が置けないようにセットする
let n = TD.innerText //セルに入ってる数字
let tmpClasses = TD.getAttribute(‘class’).split(‘ ‘)
tmpClasses.map( (classx) => {
listTD = document.querySelectorAll(‘.’ + classx)
listTD.forEach(cell => {
cell.setAttribute(‘is’ + n, 1)
})
})
}
function getAbleNum(TD){
// そのマスに可能な数字のリストを得る
let outlist = new Array()
list19.forEach(n => {
if(TD.getAttribute(‘is’ + n) == 0){
outlist.push(n)
}
})
return outlist
}
function checkMustCell(){
// 残りのマスをチェックし、一つしか数が入れられないマスに数を入れる。
let listTD = document.querySelectorAll(“[done=’0′]”)
listTD.forEach(cell => {
let checklist = new Array()
list19.forEach(n => {
if(cell.getAttribute(‘is’ + n) == 0){
checklist.push(n)
}
})
if (checklist.length == 1){
cell.append(checklist[0])
cell.setAttribute(‘done’, ‘1’)
setDisAbleCell(cell)
}
})
}
function setNum(cell, num){
// セルに数字を入れて、他のセルの処理をする。
cell.append(num)
cell.setAttribute(‘done’, ‘1’)
setDisAbleCell(cell)
checkMustCell()
}
function putNumber(){
myTable = document.getElementsByTagName(‘table’)[0]
//まず、左上、中央、右下のブロックに数字を入れてしまう。
list02.forEach(i => { //行方向のブロック番号
let rndlist19 = getRndlist19()
let idx = 0
list02.forEach(j =>{
list02.forEach(k =>{
let cell = myTable.getElementsByTagName(‘tr’)[i*3+j].getElementsByTagName(‘td’)[i*3 + k]
if(cell.innerText == ”){
setNum(cell, rndlist19[idx])
}
idx++
})
})
})
// 残りのマスに数を入れていく
let listTD = document.querySelectorAll(“[done=’0′]”)
listTD.forEach(cell =>{
let ablelist = getAbleNum(cell)
if(ablelist.length > 0){
setNum(cell, ablelist[Math.floor(Math.random()*ablelist.length)])
}
})
}
function checkTD(chkclass){
// クラスで指定された行、列、ブロックに含まれるマスの数字をチェックする
let tmpflag = 0
let checklist = new Array()
let listTD = document.querySelectorAll(chkclass)
listTD.forEach(cell =>{
checklist.push(cell.innerText)
})
checklist.sort()
if(checklist.toString() != list19.toString()){
tmpflag = 1
}
return tmpflag
}
function checkComplete(){
let checkflag = 0
// 行ごとのチェック
for(i of list08){
checkflag = checkTD(‘.r’ + i)
if(checkflag){
break
}
}
// 列ごとのチェック
if(checkflag == 0){
for(i of list08){
checkflag = checkTD(‘.c’ + i)
if(checkflag){
break
}
}
}
// ブロックごとのチェック
if(checkflag == 0){
for(i of list02){
for(j of list02){
checkflag = checkTD(‘.b’ + i + j)
if(checkflag){
break
}
}
}
}
return checkflag
}
do{
init()
putNumber()
checkflag = checkComplete()
if(checkflag == 1){
document.getElementsByTagName(‘table’)[0].remove()
}
} while (checkflag)
</script>
</body>
</html>
✅ 実行結果と所感
このプログラムを実行すると…

体感としては、数秒以内に正しい盤面が生成されます。
表示もスムーズで、ユーザー側の待機時間によるストレスもほとんどありません。
✅ 今回のまとめ
今回のプログラムでは、完成したナンプレの盤面が正しいかどうかを複数回チェックし、問題がなければ表示する仕組みを作りました。
実際に動かしてみると、成功した盤面は体感的にすぐに表示されるため、スムーズに動作します。
これでパズルの正解部分はできたので、次は空欄を上手く作って遊べるパズルにしていく予定です。
🔜 次回予告
いよいよ次回は、ナンプレプログラムの完結編です!
完成した正解表から数字を抜いて、実際に遊べる「パズル問題」を自動生成する仕組みを作っていきます。
「解ける問題になっているか?」を自動で判定するロジックや、空欄の作り方の工夫など、プログラムとしての面白さが
より深まるフェーズです。
シリーズの集大成として、ナンプレ生成の仕組みを仕上げていきますので、ぜひお楽しみに!
それではまた次回👋
最後までお読みいただき、ありがとうございました!