Developer

さくさく理解する Godot 入門(ただし2Dに限る)応用編 シーズン2(数独パズル)【第7回】
2021.11.29
Lv1

さくさく理解する Godot 入門(ただし2Dに限る)応用編 シーズン2(数独パズル)【第7回】

目次

  • 重複チェック
  • 使い切った数字ボタンディセーブル
  • クリア判定
  • グローバル変数

■重複チェック

数独では縦・横・3×3ブロックには同じ数字を入れてはいけない。 そういう状態になったときは、すぐにわかった方がよいので、数字が重複している場合は、それらを赤色表示することにする。

まずは、下記のように、数字を入れた・消した場合に check_cell_numbers() をコールする。

func cell_pressed(x, y):    # 盤面セルがクリックされた場合
    .....
    else:   # 手がかり数字以外のセルがクリックされた場合
        .....
        check_cell_numbers()    # 重複チェック
        update_cell_cursor()    # 選択数字のセル強調

check_cell_numbers() の実装は下記のようになる。

var badNumCount = 0     # 重複エラー数
.....
func is_OK(x, y, n):    # 入れた数字に重複が無いかどうかチェック
    for i in range(9):
        if i != x && get_cell_number(i, y) == n:
            return false
        if i != y && get_cell_number(x, i) == n:
            return false
        var x0 = x - x % 3
        var y0 = y - y % 3
        for v in range(3):
            for h in range(3):
                if x != x0 + h && y != y0 + v && get_cell_number(x0+h, y0+v) == n:
                    return false
    return true
func check_cell_numbers():
    badNumCount = 0
    for y in range(9):
        for x in range(9):
            var n = get_cell_number(x, y)
            if n != 0:
                if !is_OK(x, y, n):
                    badNumCount += 1
                    n += 9
                if !is_clue_cell(x, y):
                    set_cell_number(x, y, n)
                else:
                    set_cell_clue(x, y, n)

is_OK(x, y, n) は入れた数字に縦・横・3×3ブロック内で重複がないかどうかをチェックする関数だ。 単純に入れた箇所以外全部と比較し、ひとつでも一致があればその時点で false を返し、一致がなければ true を返す。

check_cell_numbers() は全部の箇所に対して is_OK() を呼んでいる。
エラーがあった場合は、後でクリア判定に使用するので badNumCount でエラー数をカウントしておく。 また、赤数字は通常数字の直後にあるので、タイルIDを+9している。

以上を実装し、実行して3を重複位置に入れると、下図のような表示になる。

■使い切った数字ボタンディセーブル

はじめから入っている手がかり数字も含めて、ある数字を9個入れた場合、その数字をそれ以上入れる必要はない。 なので、使い切った数字ボタンはディセーブルするのが親切なUIというものだ。

そのために、usedNums という配列を用意し、そこに各数字を何個使ったかを保持することにする。 なお、usedNums[0] が ‘1’ ボタンに対応する。

下記のように最初に、usedNums を宣言し、手がかり数字を設定する set_quest() で各手がかり数字の数をカウントしておく。

var usedNums = []   # 1~9 の数字を何個使用しているか
.....
func set_quest(q):
    usedNums.resize(9)
    for i in range(9):
        usedNums[i] = 0
    var ix = 0
    for ch in q:
        if ch != ' ':   # 空白は無視
            .....
            var n = ch as int
            if n >= 1 && n <= 9:
                usedNums[n-1] += 1      # 使用数字数をカウント
            .....

次に、下記のように cell_pressed() の中で、数字を入れた場合、消した場合に usedNums[] を更新する。

func cell_pressed(x, y):    # 盤面セルがクリックされた場合
    var n = get_cell_number(x, y)   # クリックされたセルに入っていた数字
    .....
    else:   # 手がかり数字以外のセルがクリックされた場合
        if n != 0:
            usedNums[n-1] -= 1          # 数字使用数を1減らす
        if get_cell_number(x, y) == cur_numButton:
            set_cell_number(x, y, 0)    # 0 for クリア
        else:
            usedNums[cur_numButton-1] += 1  # 数字使用数を1増やす
            set_cell_number(x, y, cur_numButton)
        check_cell_numbers()    # 重複チェック
        update_cell_cursor()    # 選択数字のセル強調
        update_numbuttons_disabeled()   # 数字ボタンディセーブル状態更新

最後に下記に update_numbuttons_disabeled() の実装を示す。 使い切っている場合は set_disabled() によりボタンをディセーブル状態にする。

func update_numbuttons_disabeled(): # 数字ボタンディセーブル状態更新
    for i in range(9):
        var b : bool = usedNums[i] == 9
        get_numbutton(i+1).set_disabled(b)

そうすると、’1′ を使い切った場合は、下図のように、数字ボタン1がディセーブル表示となる。

■クリア判定

重複エラーがなく、全ての数字を使い切ったら問題クリア状態だと判定できる。
そのコードは下記のように記述できる。

func is_solved():
    if badNumCount != 0:    # 重複がある場合
        return false;
    for i in range(9):
        if usedNums[i] != 9:
            return false;   # 使い切っていない数字が残っている
    return true;

本アプリでは、問題クリア時にその旨を示す確認ダイアログを表示する。

確認ダイアログを表示するクラスは AcceptDialog なので、それをノードツリーに追加する。
AcceptDialog はノードに追加すると、下図のようにノードツリーの右端に「}」を右に90度回転したようなアイコンが表示される。 これはデフォルトでは非表示という意味だ。逆に ◎ のようなアイコンは表示中という意味だ。

ダイアログを実際に表示するには、下記のように、ダイアログタイトル、文言を設定し、popup_centered() をコールするだけだ。

func cell_pressed(x, y):    # 盤面セルがクリックされた場合
    var n = get_cell_number(x, y)   # クリックされたセルに入っていた数字
    .....
    else:   # 手がかり数字以外のセルがクリックされた場合
        if is_solved():
            print("solved")
            $AudioSolved.play()
            $AcceptDialog.window_title = "Congratulations !"
            $AcceptDialog.dialog_text = "Good Job !, you are great."
            $AcceptDialog.popup_centered()

グローバル変数

GDScript にはグローバル変数が無い。 それが何故なのかはよくわからないが、ユーザ得点のようにシーン遷移を行った場合でも情報をシーン間で共有可能にしたいというのは、 当然の要求仕様だ。なので、グローバル変数と同等の機能を実現するための仕組みがある。それが「自動読み込み(AutoLoad)」だ。

まずはグローバル変数のためのシーンを作成する。ルートノードは Node2D とし、シーン名は Global としておく。
プロジェクト > プロジェクト設定… メニューを実行し、「自動読み込み」タブを選ぶ。 パス部分にグローバル変数シーンを指定し、【追加】ボタンを押す。そうすると下図のようになる。

以上で、各シーンにおいて、「Global.メンバ変数名」で Global のメンバ変数にアクセスすることが出来る。

本アプリでは、下記のように問題番号と問題データをグローバル変数として定義している。

var qNumber = 1     # 問題番号 [1, 5]
var quest = [       # 問題データ
    "008010240 090320061 102805007 039452700 670103092 001679380 900706108 780091020 015030600",
    "007643890 009000047 384570102 901065008 405208603 800130709 108027534 590000200 042351900",
    "003504900 096000750 040000080 000609000 600030001 000401000 080000020 054000160 007108500",
    "100800007 007000050 030907100 504020900 000604000 009070304 001503040 040000600 700009003",
    "009060070 000000015 600051000 000007300 706000509 003200000 000980003 890000000 020040800",
]