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}