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

こんにちは!
エルフィールドでエンジニアとして働いている、H.Tです。
ナンバープレイスをテーマにしたプログラム作成シリーズの第3回です。
前回のプログラムでは、マスの順番に数字を埋めていく方式を採用しましたが、完成できないケースが多く発生しました。
そこで今回は、発想を逆転して、「マスを決めて数字を入れる」→「数字を決めて、それを入れられるマスを探す」
という方向に切り替えます。
実際にナンプレを解くときも、数字ごとに空き場所を絞るやり方は有効ですよね。
■ 設計の考え方
前回まではマスの状態を特に管理していませんでしたが、今回は以下のような独自属性(フラグ)をTDタグに追加し、
状態を明示的に管理することにします。
属性名 | 意味 |
---|---|
done | 数字が入力済みなら1、未入力なら0 |
is1 ~is9 | 各数字がそのマスに入れられないなら1、入れられるなら0 |
■ プログラムの抜粋
● 数字1~9の配列
const list19 = [1,2,3,4,5,6,7,8,9]
● 表の初期化(属性付与)
tmpTD.setAttribute(‘done’, 0)
list19.forEach(n => {
tmpTD.setAttribute(‘is’ + n, 0)
■ 主な関数と処理内容
● 数字を入れたときに他のマスを制約する関数
function setDisAbleCell(TD) {
let n = TD.innerText
let tmpClasses = TD.getAttribute(‘class’).split(‘ ‘)
tmpClasses.map(classx => {
let listTD = document.querySelectorAll(‘.’ + classx)
listTD.forEach(cell => {
cell.setAttribute(‘is’ + n, 1)
})
})
}
● あるマスに入れられる数字の一覧を取得する関数
function getAbleNum(TD) {
let outlist = []
list19.forEach(n => {
if (TD.getAttribute(‘is’ + n) == 0) {
outlist.push(n)
}
})
return outlist
}
● 確定マス(1種類しか入れられない)をチェックして埋める関数
function checkMustCell() {
let listTD = document.querySelectorAll(“[done=’0′]”)
listTD.forEach(cell => {
let checklist = []
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)
setDisAbleCell(cell)
cell.setAttribute(‘done’, 1)
checkMustCell()
}
数字(1〜9)を先に決めて、各3×3ブロックに対して、その数字を入れられるマスを探してランダムに配置します。
■全体コード
<!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 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)
setDisAbleCell(cell)
cell.setAttribute(‘done’, 1)
checkMustCell()
}
function putNumber(){
myTable = document.getElementsByTagName(‘table’)[0]
list19.forEach(n => { //マスに入れる数字
list02.forEach(i => { //行方向のブロック番号
list02.forEach(j => { //列方向のブロック番号
let ablelist = new Array() //数字を入れられるマスのリスト
let listTD = document.querySelectorAll(‘.b’ + i + j) //ブロック内の全てのマス
listTD.forEach(cell => {
if(cell.getAttribute(‘done’) == 0 && cell.getAttribute(‘is’ + n) == 0){
ablelist.push(cell)
}
})
let selectedCell = ablelist[Math.floor(Math.random() * ablelist.length)]
setNum(selectedCell,n)
})
})
})
}
init()
putNumber()
</script>
</body>
</html>
これを実行すると・・・

出来ていない!!さらに・・・

結果は……失敗!!!
なんと、「2」の時点で詰んでしまうパターンが出てきました。
確率に任せた配置方法では、どうしても詰みが避けられない状況が生まれます。
■ 気づきと今後の展望
今回のチャレンジでは、
- 発想を切り替えてマスの選び方を工夫した
- 状態を属性で明示的に管理する方法に取り組んだ
という新しいアプローチを試しましたが、結果的にはまだ「完全な盤面を作る」には至りませんでした。
「もう答えだけ見せてほしい!」
と思ったあなた、大丈夫です。筆者もそう思っています(笑)でも今回はもう少しだけ寄り道をさせてください。
このシリーズの目的はあくまでも試行錯誤のプロセスを共有することです。
次回こそ、「どうすれば詰まらない盤面が作れるか?」という核心に迫っていきたいと思います!
■ 次回予告
・確実に埋まる盤面を作るにはどうすればいいのか?
・今回のような「先行入力+制約チェック」方式は改良の余地があるのか?
・いよいよ本格的な探索アルゴリズム導入か…?
というあたりをテーマに、次回はさらに深掘りしていきます。
それでは、また次回!👋