nodes/gameboard.pyΒΆ

Part of GDQuest Open RPG.

  1"""Hand-authored overworld map and BFS pathfinder."""
  2
  3from __future__ import annotations
  4
  5from collections import deque
  6
  7from .settings import GRID_H, GRID_W, TILE
  8
  9# Tile codes
 10GRASS = 0
 11PATH = 1
 12WATER = 2
 13WALL = 3
 14TREE = 4
 15SAND = 5
 16SHRINE = 6
 17
 18# Walkability
 19WALKABLE = {GRASS, PATH, SAND, SHRINE}
 20
 21
 22def _make_map() -> list[list[int]]:
 23    m = [[GRASS for _ in range(GRID_W)] for _ in range(GRID_H)]
 24    # river along top
 25    for x in range(0, GRID_W):
 26        m[0][x] = WATER
 27    # bridge / path
 28    for y in range(1, GRID_H):
 29        m[y][GRID_W // 2] = PATH
 30    for x in range(2, GRID_W - 2):
 31        m[8][x] = PATH
 32    # tree border bottom-left
 33    for x in range(2, 8):
 34        m[14][x] = TREE
 35    for y in range(11, 15):
 36        m[y][2] = TREE
 37    # walls / building (top-right)
 38    for y in range(2, 6):
 39        for x in range(GRID_W - 6, GRID_W - 2):
 40            m[y][x] = WALL
 41    for x in range(GRID_W - 5, GRID_W - 3):
 42        m[5][x] = PATH  # door
 43    # sand patch
 44    for y in range(11, 14):
 45        for x in range(20, 25):
 46            m[y][x] = SAND
 47    # save shrine
 48    m[10][3] = SHRINE
 49    # bottom water row
 50    for x in range(0, GRID_W):
 51        if x not in (GRID_W // 2,):
 52            m[GRID_H - 1][x] = WATER
 53    return m
 54
 55
 56TILES: list[list[int]] = _make_map()
 57
 58
 59def in_bounds(cx: int, cy: int) -> bool:
 60    return 0 <= cx < GRID_W and 0 <= cy < GRID_H
 61
 62
 63def is_walkable(cx: int, cy: int) -> bool:
 64    if not in_bounds(cx, cy):
 65        return False
 66    return TILES[cy][cx] in WALKABLE
 67
 68
 69def cell_to_pixel(cx: int, cy: int) -> tuple[float, float]:
 70    return cx * TILE + TILE * 0.5, cy * TILE + TILE * 0.5
 71
 72
 73def pixel_to_cell(px: float, py: float) -> tuple[int, int]:
 74    return int(px // TILE), int(py // TILE)
 75
 76
 77def bfs_path(src: tuple[int, int], dst: tuple[int, int]) -> list[tuple[int, int]]:
 78    """Return list of cells from `src` to `dst` (inclusive). Empty if unreachable."""
 79    if src == dst or not is_walkable(*dst):
 80        return []
 81    came: dict[tuple[int, int], tuple[int, int] | None] = {src: None}
 82    q = deque([src])
 83    while q:
 84        cur = q.popleft()
 85        if cur == dst:
 86            break
 87        cx, cy = cur
 88        for nx, ny in ((cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)):
 89            n = (nx, ny)
 90            if n not in came and is_walkable(nx, ny):
 91                came[n] = cur
 92                q.append(n)
 93    if dst not in came:
 94        return []
 95    path: list[tuple[int, int]] = []
 96    cur: tuple[int, int] | None = dst
 97    while cur is not None:
 98        path.append(cur)
 99        cur = came[cur]
100    return list(reversed(path))[1:]  # exclude src
101
102
103# Map of which cell triggers a battle encounter, dialogue, or shrine.
104# (cell): ('encounter', 'wolves')  | ('dialogue', 'monk')  | ('save')
105TRIGGERS: dict[tuple[int, int], tuple[str, str]] = {
106    (GRID_W // 2, 5): ("encounter", "wolves"),
107    (3, 10): ("save", ""),
108    (15, 8): ("encounter", "bears"),
109    (24, 12): ("encounter", "bugcats"),
110}
111
112# NPC spawns: cell -> (kind, dialogue_id)
113NPCS: list[dict] = [
114    {"cell": (5, 8), "kind": "monk", "dialogue": "monk"},
115    {"cell": (10, 4), "kind": "smith", "dialogue": "smith"},
116    {"cell": (22, 9), "kind": "wizard_npc", "dialogue": "wizard_npc"},
117]
118
119
120# Pre-baked dialogue scripts (speaker, text)
121DIALOGUES: dict[str, list[tuple[str, str]]] = {
122    "monk": [
123        ("Monk", "Greetings, traveller."),
124        ("Monk", "Beware the wolves at the bridge."),
125        ("Monk", "And the bears past the river bend."),
126    ],
127    "smith": [
128        ("Smith", "I'm too busy to chat."),
129        ("Smith", "...fine. Don't lose your sword."),
130    ],
131    "wizard_npc": [
132        ("Old Mage", "Have you tried casting spells?"),
133        ("Old Mage", "Energy refills as you take damage. A poor trade, perhaps."),
134    ],
135    "encounter_wolves": [
136        ("Wolf", "*growls menacingly*"),
137        ("Knight", "Form up! Two on the right."),
138    ],
139    "encounter_bears": [
140        ("Bear", "*roars*"),
141        ("Knight", "Watch out for the big one!"),
142    ],
143    "encounter_bugcats": [
144        ("Bugcat", "*chitters*"),
145        ("Squirrel", "Easy pickings."),
146    ],
147    "victory": [
148        ("Knight", "We won!"),
149        ("Knight", "Onward."),
150    ],
151    "defeat": [
152        ("Knight", "We lost..."),
153        ("Knight", "Back to the shrine."),
154    ],
155    "save": [
156        ("Shrine", "Your party is restored."),
157        ("Shrine", "Progress saved."),
158    ],
159}