Coverage for src/glomph/loaders.py: 89%
135 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-05 21:51 +0800
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-05 21:51 +0800
1"""Asset loaders for mazes, sprites, and tiles."""
3from pathlib import Path
4from typing import NamedTuple
7class MazeData(NamedTuple):
8 """Maze file data."""
10 width: int
11 height: int
12 flags: int
13 content: str
14 metadata: dict[str, str]
17class TileData(NamedTuple):
18 """Tile file data."""
20 width: int
21 height: int
22 flags: int
23 tiles: dict[str, list[str]]
24 metadata: dict[str, str]
27class SpriteData(NamedTuple):
28 """Sprite file data."""
30 width: int
31 height: int
32 flags: int
33 frames: dict[str, list[str]]
34 metadata: dict[str, str]
37class MazeLoader:
38 """Load maze files from assets/mazes/."""
40 def __init__(self, assets_dir: Path) -> None:
41 """Initialize maze loader."""
42 self.mazes_dir = assets_dir / "mazes"
44 def load(self, name: str) -> MazeData:
45 """Load a maze file by name."""
46 path = self.mazes_dir / f"{name}.txt"
47 if not path.exists(): 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true
48 path = self.mazes_dir / f"{name}.asc"
50 if not path.exists(): 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 raise FileNotFoundError(f"Maze not found: {name}")
53 with open(path, encoding="utf-8") as f:
54 lines = f.readlines()
56 # Parse first line: "N WxH" or "N WxH~F arg1=val1 ..."
57 header = lines[0].strip().split()
58 dimensions = header[1].split("x")
59 width = int(dimensions[0])
61 # Height and flags
62 height_and_flags = dimensions[1].split("~")
63 height = int(height_and_flags[0])
64 flags = int(height_and_flags[1]) if len(height_and_flags) > 1 else 0
66 # Parse metadata arguments
67 metadata: dict[str, str] = {}
68 if len(header) > 2: 68 ↛ 75line 68 didn't jump to line 75 because the condition on line 68 was always true
69 for arg in header[2:]:
70 if "=" in arg:
71 key, value = arg.split("=", 1)
72 metadata[key] = value
74 # Content is everything after first line
75 content = "".join(lines[1:])
77 return MazeData(width, height, flags, content, metadata)
79 def list_mazes(self) -> list[str]:
80 """List all available maze names."""
81 mazes = []
82 for path in self.mazes_dir.glob("*.txt"):
83 mazes.append(path.stem)
84 for path in self.mazes_dir.glob("*.asc"):
85 if path.stem not in mazes: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 mazes.append(path.stem)
87 return sorted(mazes)
90class TileLoader:
91 """Load tile files from assets/tiles/."""
93 def __init__(self, assets_dir: Path) -> None:
94 """Initialize tile loader."""
95 self.tiles_dir = assets_dir / "tiles"
97 def load(self, name: str) -> TileData:
98 """Load a tile file by name."""
99 path = self.tiles_dir / f"{name}.txt"
100 if not path.exists(): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 path = self.tiles_dir / f"{name}.asc"
103 if not path.exists(): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 raise FileNotFoundError(f"Tile file not found: {name}")
106 with open(path, encoding="utf-8-sig") as f:
107 content = f.read()
109 # Parse header: "WxH" or "WxH~F arg1=val1 ..."
110 lines = content.split("\n")
111 header = lines[0].strip().split()
112 dimensions = header[0].split("x")
113 width = int(dimensions[0])
115 height_and_flags = dimensions[1].split("~")
116 height = int(height_and_flags[0])
117 flags = int(height_and_flags[1]) if len(height_and_flags) > 1 else 0
119 # Parse metadata
120 metadata: dict[str, str] = {}
121 if len(header) > 1: 121 ↛ 128line 121 didn't jump to line 128 because the condition on line 121 was always true
122 for arg in header[1:]:
123 if "=" in arg:
124 key, value = arg.split("=", 1)
125 metadata[key] = value
127 # Parse tiles
128 tiles: dict[str, list[str]] = {}
129 i = 1
130 while i < len(lines):
131 line = lines[i].strip()
132 if not line:
133 i += 1
134 continue
136 # Tile header: "XX~Y" or "XX"
137 tile_code = line.split("~")[0]
139 # Read tile lines
140 tile_lines = []
141 for j in range(height):
142 if i + 1 + j < len(lines): 142 ↛ 141line 142 didn't jump to line 141 because the condition on line 142 was always true
143 tile_line = lines[i + 1 + j]
144 if tile_line.startswith(":"): 144 ↛ 141line 144 didn't jump to line 141 because the condition on line 144 was always true
145 tile_lines.append(tile_line[1 : width + 1])
147 tiles[tile_code] = tile_lines
148 i += height + 1
150 return TileData(width, height, flags, tiles, metadata)
153class SpriteLoader:
154 """Load sprite files from assets/sprites/."""
156 def __init__(self, assets_dir: Path) -> None:
157 """Initialize sprite loader."""
158 self.sprites_dir = assets_dir / "sprites"
160 def load(self, name: str) -> SpriteData:
161 """Load a sprite file by name."""
162 path = self.sprites_dir / f"{name}.txt"
163 if not path.exists(): 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 path = self.sprites_dir / f"{name}.asc"
166 if not path.exists(): 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 raise FileNotFoundError(f"Sprite file not found: {name}")
169 with open(path, encoding="utf-8-sig") as f:
170 content = f.read()
172 # Parse similar to tiles
173 lines = content.split("\n")
174 header = lines[0].strip().split()
175 dimensions = header[0].split("x")
176 width = int(dimensions[0])
178 height_and_flags = dimensions[1].split("~")
179 height = int(height_and_flags[0])
180 flags = int(height_and_flags[1]) if len(height_and_flags) > 1 else 0
182 metadata: dict[str, str] = {}
183 if len(header) > 1: 183 ↛ 190line 183 didn't jump to line 190 because the condition on line 183 was always true
184 for arg in header[1:]:
185 if "=" in arg:
186 key, value = arg.split("=", 1)
187 metadata[key] = value
189 # Parse sprite frames (same format as tiles)
190 frames: dict[str, list[str]] = {}
191 i = 1
192 while i < len(lines):
193 line = lines[i].strip()
194 if not line:
195 i += 1
196 continue
198 frame_code = line.split("~")[0]
199 frame_lines = []
200 for j in range(height):
201 if i + 1 + j < len(lines): 201 ↛ 200line 201 didn't jump to line 200 because the condition on line 201 was always true
202 frame_line = lines[i + 1 + j]
203 if frame_line.startswith(":"): 203 ↛ 200line 203 didn't jump to line 200 because the condition on line 203 was always true
204 frame_lines.append(frame_line[1 : width + 1])
206 frames[frame_code] = frame_lines
207 i += height + 1
209 return SpriteData(width, height, flags, frames, metadata)