Developer

さくさく理解する Godot 入門(ただし2Dに限る)応用編 お絵かきパズル【第9回】
2021.12.21
Lv1

さくさく理解する Godot 入門(ただし2Dに限る)応用編 お絵かきパズル【第9回】

目次

  • 問題を解く
  • 問題設定

問題を解く

前回までで、お絵かきパズルの画面作成、パズル問題作成機能を実装した。
今回からは、指定された問題を表示し、それをユーザが解けるようにする機能を実装していく。
ユーザがクリックしたセルに黒またはバツを入れる処理は問題作成の章ですでに示したので、 ここでは問題設定、使い切った・間違った手がかり数字強調、ヒント機能の実装について解説していく。

■問題設定

パズルの問題は、下記のように問題情報、上左部の手がかり数字を配列で表すものとする。

    ["Q060", 16, "Kyoro-chan", "vivisuke",
    [" 0"," 4"," 2 2"," 1 2"," 2 3"," 4 2 1"," 1 1 2 1 2"," 1 3 2 1"," 9 1"," 2 1 1 2"," 2 2 2"," 6 2"," 9"," 5"," 0",],
    [" 3"," 1 3"," 3 4"," 3 1 2"," 1 5 2"," 1 3 3"," 1 7"," 1 1 4"," 1 2 2"," 2 3 2"," 4 1"," 2 2"," 1 2"," 6"," 1 1",],]

最初は問題固有のID文字列、次は難易度、そして問題タイトルと作者名だ。
手がかり数字は1行・列ごとにひとつの文字列で表し、それを要素数15個の配列となっている。
このように GDScript は動的型言語で、配列に異なった型を入れることができ、とても便利だ。

ちなみに、上記問題を表示すると下図のようになる。

下記に、問題を設定する func set_quest(vq, hq) の実装を示す。

func to_clues(txt : String):
    if txt.empty():     # 手がかり数字が空の場合 → 0 とみなす
        txt = " 0"
    if (txt.length() % 2) == 1:     # 手がかり数字文字列長が奇数の場合
        txt = " " + txt
    var lst = []
    while !txt.empty():
        lst.push_front(int(txt.left(2)))    # 先頭2文字を数値に変換し、lst に保存
        txt = txt.substr(2)                 # 先頭2文字削除
    return lst
# 問題設定
func set_quest(vq, hq):     # vq: 上部縦方向手がかり数字文字列、hq: 左部横方向手がかり数字文字列
    for x in range(N_IMG_CELL_HORZ):    # 上部手がかり数字
        var lst
        if x < vq.size():       # 手がかり数字が残っている場合
            lst = to_clues(vq[x])
        else:
            lst = [0]
        v_clues[x] = lst
        update_v_cluesText(x, lst)      # 上部手がかり数字表示
    for y in range(N_IMG_CELL_VERT):    # 左部手がかり数字
        var lst
        if y < hq.size():       # 手がかり数字が残っている場合
            lst = to_clues(hq[y])
        else:
            lst = [0]
        h_clues[y] = lst
        update_h_cluesText(y, lst)      # 左部手がかり数字表示

最初に1行または1列の手がかり数字文字列を手がかり数字配列に変換する to_clues(txt) を定義している。 最初に文字列が空の場合は " 0" にし、文字列長が奇数の場合は先頭に " " を付加している。 あとは2文字ごとに取り出し、int() で整数に変換し lst.push_front() で配列先頭に追加している。 push_back() ではなく push_front() を用いているのは、配列要素順の手がかり数字文字列とは逆順にするためだ。 何故逆順にしているかというと、手がかり数字は縦方向は下詰め、横方向は右詰めだし、 タイルマップの座標が下・右から -1, -2, -3, … と減少していくので、そうしておくほうがコードが簡単になるからだ。

set_quest(vq, hq) の実装は単純で、行・列のそれぞれを to_clues() を呼んで数値配列に変換し、 update_h_cluesText(), update_v_cluesText() でタイルマップに反映させている。
これらの実装は下記の通りだ。

func update_v_cluesText(x0, lst):
    var y = -1
    for i in range(lst.size()):			# 手がかり数字配列が存在する間
        $BoardBG/TileMap.set_cell(x0, y, lst[i] + TILE_NUM_0 if lst[i] != 0 else TILE_NONE)
        y -= 1
    while y >= -N_CLUES_CELL_VERT:		# 手がかり数字が残っている場合
        $BoardBG/TileMap.set_cell(x0, y, TILE_NONE)
        y -= 1

以上で、準備が整ったので、初期化関数である _ready() から set_quest() をコールする。

func _ready():
    .....
    set_quest(g.quest_list[qix][g.KEY_V_CLUES], g.quest_list[qix][g.KEY_H_CLUES])

g.quest_list はこの章の最初に示した問題データの配列で、下記のように定義されている。

enum {
    KEY_ID = 0,
    KEY_DIFFICULTY,
    KEY_TITLE,
    KEY_AUTHOR,
    KEY_V_CLUES,
    KEY_H_CLUES,
    }
var quest_list = [     # 非ソート済み問題配列
    ["Q001", 1, "PC Net", "matoya",
    ["15", "1 1 8", "1 2 1 8", "1 2 8", "1 2 1 8",  "1 1 2 4", "8 2 3", "3 3 3 3", "3 2 8", "3 1 2 1 1",    "4 3 2 1 1", "8 2 1", "8 2 1 1", "8 1 1", "15"],
    ["15", "1 9", "1 3 9", "1 3 1 5", "1 4 4",  "3 5 5", "1 2 6", "7 7", "6 2 1", "5 3 3 1",    "5 3 3 1", "6 1 1", "11 3", "9 1", "15"]],
    .....

C/C++ の様に静的型付け言語では、上記の様に配列リテラルに異なる型のデータを記述することは出来ず、 そのための構造体を下記の様に定義しなくてはいけない。

struct Quest {
    string  m_id;               //  問題ID
    int     m_difficulty;       //  難易度
    string  m_title;            //  問題タイトル
    string  m_author;           //  作者名
    vector  m_clues1;
    vector  m_clues2;
};

これに対して GDScript は動的型付け言語なので、本稿の様に構造体宣言無しに配列リテラルに様々な型を自由に記述することができる。 ただし、各要素にアクセスするときに quest_list[qix][1] の様に記述すると、 「1」が何を意味しているのか不明確で文書生が低下し危険なので、各配列インデックスを enum で宣言し、 「quest_list[qix][KEY_DIFFICULTY]」の様に記述する方が何をアクセスしているのか読んですぐ分かるので安全だ。