【プログラムを作ろう】 ナンプレ編 #6(完結)

こんにちは!
エルフィールドでエンジニアとして働いている、H.Tと申します。
今回はナンプレプログラムの第6回、いよいよ完結編です!
前回までで、正しく完成された盤面(答えの状態)を自動で生成できるようになりました。
いよいよ今回は、「実際に遊べるナンプレ問題」に仕上げていきます。
🔧 設計
今回の処理の流れは以下の通りです:
- 前回までの方法で、数字がすべて入った盤面(正解表)を作成
- ランダムにマスを選んで空白にする
- フラグを初期化し、数字が入っているマスからフラグを立て直す
- 解けるかどうかをチェック
- 解けたら空白マスだけを戻して「問題」とする。解けなければ②に戻る
この処理を繰り返し実行し、成功した問題だけが表示されるようにします。
🎨 文字色の設定
パズルが解けたときに、空白に入れた数字が見分けられるように、赤文字で表示したいと思います。
CSSに以下のスタイルを追加します:
#app td[color=”red”] {
color: red;
}
また、HTMLの各マスにcolor=”red”を設定しておけば、CSSでその色を適用できます。
🔲 空欄の作成
空白を1つ作る関数は以下のようになります:
function removeNum() {
let i = list08[Math.floor(Math.random() * 9)];
let j = list08[Math.floor(Math.random() * 9)];
let tmpTD = document.querySelectorAll(‘.r’ + i + ‘.c’ + j)[0];
if (tmpTD.innerText != ”) {
tmpTD.innerText = ”;
tmpTD.setAttribute(‘done’, 0);
tmpTD.setAttribute(‘color’, ‘red’);
}
}
「i」と「j」を0〜8からランダムに選び、class指定でマスを選択しています。
選ばれたマスの中身が空でなければ削除し、フラグをリセットします。
空白マスの数は60回ランダムに選びます。重複もあるため、実際の空白マス数は60個以下になります。
🧠 パズルを解く
コードは以下のようになっています。
function solveNP() {
// フラグ初期化
list08.forEach(i => {
list08.forEach(j => {
let tmpTD = document.querySelectorAll(‘.r’ + i + ‘.c’ + j)[0];
list19.forEach(n => {
tmpTD.setAttribute(‘is’ + n, 0);
});
});
});
// 数字が入ってるマスからフラグを立てる
let listTD1 = document.querySelectorAll(“[done=’1′]”);
listTD1.forEach(cell => {
setDisAbleCell(cell);
checkMustCell();
});
// 空欄マスを埋めていく
let listTD0 = document.querySelectorAll(“[done=’0′]”);
listTD0.forEach(cell => {
let ablelist = getAbleNum(cell);
if (ablelist.length > 0) {
setNum(cell, ablelist[Math.floor(Math.random() * ablelist.length)]);
}
});
return checkComplete();
}
すべてのフラグを初期化し、数字が入っているマスからフラグを立て直します。
その後、空欄に数字を入れていきます。
最後にcheckComplete()関数で正しく解けているかをチェックします。
🧪 ボタンで解く!
以下のように、ボタンを作成してsolveNP()関数を呼び出します。
btnSolve = document.createElement(‘button’);
btnSolve.setAttribute(‘onclick’,’solveME()’);
btnSolve.append(‘解く!’);
document.querySelector(‘#app’).append(btnSolve);
ボタンを押したらsolveNP()が実行されます。これがJavascriptらしい使い方ですね。
空白を作って解いてみると、解けない場合もありますが、解けた場合は以下のように表示されます。

解けた場合、赤文字にすれば「このマスは後から埋めた」ことが分かりますが……
ここを赤文字ではなく白文字にすれば、見えない=空欄のまま見える状態になりますよね。
そして「解く!」ボタンを押したときに白文字から赤文字にすればいいのでは?
そのため、少しだけ改修します。
🎨 色の切り替えを実装する
・CSSのstyleに白文字も追加します:
#app td[color=”white”] {
color: white;
}
・空欄作成関数removeNumの中でも、色指定を白に変更します:
tmpTD.setAttribute(‘color’, ‘white’);
・ボタンを押したときに色を変える関数を作成:
function solveME() {
let listTD = document.querySelectorAll(“[color=’white’]”);
listTD.forEach(cell => {
cell.setAttribute(‘color’,’red’);
});
}
🔁 メイン処理のロジック
// 問題を作る
do{// とりあえず60個のマスを空欄にする
for (let idx = 0; idx < 60; idx++){
removeNum()
}
checkflag = solveNP()
} while (checkflag)
この処理により、
- ランダムに空欄を作成
- 解けるかをチェック
- 解けなければやり直し
という流れを繰り返す仕組みになります。
💻 プログラム全体
<!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;}
#app td[color=”red”]{color: red;}
#app td[color=”white”]{color: white;}
</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){
setNum(cell, checklist[0])
}
})
}
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)
// 問題を作って解く
function removeNum(){
let i = list08[Math.floor(Math.random() * 9)]
let j = list08[Math.floor(Math.random() * 9)]
let tmpTD = document.querySelectorAll(‘.r’ + i + ‘.c’ + j)[0]
if(tmpTD.innerText != ”){
tmpTD.innerText = ”
tmpTD.setAttribute(‘done’, 0)
tmpTD.setAttribute(‘color’, ‘white’)
}
}
function solveNP(){
// 数字候補フラグを初期化する
list08.forEach(i => {
list08.forEach(j => {
let tmpTD = document.querySelectorAll(‘.r’ + i + ‘.c’ + j)[0]
list19.forEach(n => {tmpTD.setAttribute(‘is’ + n, 0)})
})
})
//数字が入ってるマスを見てフラグを立てていく
let listTD1 = document.querySelectorAll(“[done=’1′]”)
listTD1.forEach(cell =>{
setDisAbleCell(cell)
checkMustCell()
})
//数字が入っていないマスを解く
let listTD0 = document.querySelectorAll(“[done=’0′]”)
listTD0.forEach(cell =>{
let ablelist = getAbleNum(cell)
if(ablelist.length > 0){
setNum(cell, ablelist[Math.floor(Math.random() * ablelist.length)])
}
})
return checkComplete()
}
btnSolve = document.createElement(‘button’)
btnSolve.setAttribute(‘onclick’,’solveME()’)
btnSolve.append(‘解く!’)
document.querySelector(‘#app’).append(btnSolve)
function solveME(){
// 白文字のマスを選び出す
let listTD = document.querySelectorAll(“[color=’white’]”)
listTD.forEach(cell =>{
cell.setAttribute(‘color’,’red’)
})
}
// 問題を作る
do{// とりあえず60個のマスを空欄にする
for (let idx = 0; idx < 60; idx++){
removeNum()
}
checkflag = solveNP()
} while (checkflag)
</script>
</body>
</html>
✅ 実行結果と所感
この方式で実行すると、以下のようになります。


- 最初は空白マス(白文字)で「問題」として表示される
- 「解く!」ボタンを押すと、白文字だったマスが赤に変化し、解答が表示される
- 赤文字の数字は「後から自動で埋められたマス」であることが一目でわかる
試してみると、シンプルなロジック(可能性が1つのマスから順に埋める)でも、十分に解ける問題が自動で生成されました。
🧩 プログラミングで一番大事なこと
考えるだけではなく、「とりあえず作って試してみること」
失敗しても、試行錯誤を繰り返すことで、少しずつ形が見えてきます。
このナンプレも、思いつきを形にして、修正して、また作り直して…
そうするうちに、「ちょうど良いシステム」にまとまりました。
🏁 最後に
これまで全6回にわたって、ナンプレの自動生成と解答チェック、そして問題生成までを作ってきました。
最終的に、自動的に問題を作って、解くツールとして動作するプログラムが完成しました。
- 自動で正解表を作る
- ランダムに空欄を作る
- 解けるかどうか自動判定する
- 白→赤の仕組みで問題として成立させる
ナンプレを題材にしたこのプログラム制作を通じて、JavaScriptでのロジック設計やUI操作の勉強にもなりました。
✨ ありがとうございました!
これでナンプレ編は終了です!
最初は「表を作るだけ」だったところから、問題として遊べるところまで到達できました。
ここまで読んでいただき、本当にありがとうございました!
それではまた別のテーマでお会いしましょう!👋