WinColl 0.9.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- WinColl-0.9.6.dist-info/METADATA +138 -0
- WinColl-0.9.6.dist-info/RECORD +37 -0
- WinColl-0.9.6.dist-info/WHEEL +5 -0
- WinColl-0.9.6.dist-info/entry_points.txt +2 -0
- WinColl-0.9.6.dist-info/top_level.txt +1 -0
- wincoll/Collect.wav +0 -0
- wincoll/Slide.wav +0 -0
- wincoll/Splat.wav +0 -0
- wincoll/Unlock.wav +0 -0
- wincoll/__init__.py +643 -0
- wincoll/__main__.py +7 -0
- wincoll/acorn-mode-1.ttf +0 -0
- wincoll/diamond.png +0 -0
- wincoll/langdetect.py +60 -0
- wincoll/levels/01.tmx +61 -0
- wincoll/levels/02.tmx +61 -0
- wincoll/levels/03.tmx +61 -0
- wincoll/levels/04.tmx +61 -0
- wincoll/levels/05.tmx +61 -0
- wincoll/levels/06.tmx +61 -0
- wincoll/levels/Blob.png +0 -0
- wincoll/levels/Brick.png +0 -0
- wincoll/levels/Diamond.png +0 -0
- wincoll/levels/Earth.png +0 -0
- wincoll/levels/Gap.png +0 -0
- wincoll/levels/Key.png +0 -0
- wincoll/levels/Rock.png +0 -0
- wincoll/levels/Safe.png +0 -0
- wincoll/levels/Win.png +0 -0
- wincoll/levels/WinColl.tsx +34 -0
- wincoll/locale/el/LC_MESSAGES/wincoll.mo +0 -0
- wincoll/locale/fr/LC_MESSAGES/argparse.mo +0 -0
- wincoll/locale/fr/LC_MESSAGES/wincoll.mo +0 -0
- wincoll/ptext.py +1196 -0
- wincoll/splat.png +0 -0
- wincoll/title.png +0 -0
- wincoll/warnings_util.py +29 -0
wincoll/__init__.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# © Reuben Thomas <rrt@sc3d.org> 2024
|
|
2
|
+
# Released under the GPL version 3, or (at your option) any later version.
|
|
3
|
+
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import argparse
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import pickle
|
|
11
|
+
import warnings
|
|
12
|
+
from warnings import warn
|
|
13
|
+
from typing import Any, NoReturn, Tuple, List, Optional, Union, Iterator
|
|
14
|
+
from itertools import chain
|
|
15
|
+
import locale
|
|
16
|
+
import gettext
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
import i18nparse # type: ignore
|
|
20
|
+
import importlib_resources
|
|
21
|
+
from typing_extensions import Self
|
|
22
|
+
from platformdirs import user_data_dir
|
|
23
|
+
|
|
24
|
+
from .warnings_util import simple_warning
|
|
25
|
+
from .langdetect import language_code
|
|
26
|
+
|
|
27
|
+
locale.setlocale(locale.LC_ALL, "")
|
|
28
|
+
|
|
29
|
+
# Try to set LANG for gettext if not already set
|
|
30
|
+
if not "LANG" in os.environ:
|
|
31
|
+
lang = language_code()
|
|
32
|
+
if lang is not None:
|
|
33
|
+
os.environ["LANG"] = lang
|
|
34
|
+
i18nparse.activate()
|
|
35
|
+
|
|
36
|
+
# Set app name for SDL
|
|
37
|
+
os.environ["SDL_APP_NAME"] = "WinColl"
|
|
38
|
+
|
|
39
|
+
# Import pygame, suppressing extra messages that it prints on startup.
|
|
40
|
+
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
|
|
41
|
+
with warnings.catch_warnings():
|
|
42
|
+
warnings.simplefilter("ignore")
|
|
43
|
+
import pygame
|
|
44
|
+
import pyscroll # type: ignore
|
|
45
|
+
import pytmx # type: ignore
|
|
46
|
+
from . import ptext
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
VERSION = importlib.metadata.version("wincoll")
|
|
50
|
+
|
|
51
|
+
with importlib_resources.as_file(importlib_resources.files()) as path:
|
|
52
|
+
cat = gettext.translation("wincoll", path / "locale", fallback=True)
|
|
53
|
+
_ = cat.gettext
|
|
54
|
+
|
|
55
|
+
CACHE_DIR = Path(user_data_dir("wincoll"))
|
|
56
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
SAVED_POSITION_FILE = CACHE_DIR / "saved_position.pkl"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def die(code: int, msg: str) -> NoReturn:
|
|
61
|
+
warn(msg)
|
|
62
|
+
sys.exit(code)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
with importlib_resources.as_file(importlib_resources.files()) as path:
|
|
66
|
+
levels = len(list(Path(path / "levels").glob("*.tmx")))
|
|
67
|
+
level_size = 50 # length of side of world in blocks
|
|
68
|
+
block_pixels = 16 # size of (square) block sprites in pixels
|
|
69
|
+
window_blocks = 15
|
|
70
|
+
window_pixels = window_blocks * block_pixels
|
|
71
|
+
window_scale = 2
|
|
72
|
+
scaled_pixels = window_pixels * window_scale
|
|
73
|
+
TEXT_COLOUR = (255, 255, 255)
|
|
74
|
+
BACKGROUND_COLOUR = (0, 0, 255)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def flash_background() -> None:
|
|
78
|
+
global BACKGROUND_COLOUR
|
|
79
|
+
BACKGROUND_COLOUR = (160, 160, 255)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def fade_background() -> None:
|
|
83
|
+
global BACKGROUND_COLOUR
|
|
84
|
+
BACKGROUND_COLOUR = (
|
|
85
|
+
max(BACKGROUND_COLOUR[0] - 10, 0),
|
|
86
|
+
max(BACKGROUND_COLOUR[0] - 10, 0),
|
|
87
|
+
255,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_image(filename: str) -> pygame.Surface:
|
|
92
|
+
with importlib_resources.as_file(importlib_resources.files()) as path:
|
|
93
|
+
return pygame.image.load(path / filename)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
DIAMOND_IMAGE = load_image("diamond.png")
|
|
97
|
+
SPLAT_IMAGE = load_image("splat.png")
|
|
98
|
+
TITLE_IMAGE = load_image("title.png")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
COLLECT_SOUND: pygame.mixer.Sound
|
|
102
|
+
SLIDE_SOUND: pygame.mixer.Sound
|
|
103
|
+
UNLOCK_SOUND: pygame.mixer.Sound
|
|
104
|
+
SPLAT_SOUND: pygame.mixer.Sound
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
screen: pygame.Surface
|
|
108
|
+
|
|
109
|
+
app_icon = load_image("levels/Win.png")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def init_screen(flags: int = pygame.SCALED) -> None:
|
|
113
|
+
global screen
|
|
114
|
+
pygame.display.set_icon(app_icon)
|
|
115
|
+
screen = pygame.display.set_mode((640, 512), flags)
|
|
116
|
+
reinit_screen()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def reinit_screen() -> None:
|
|
120
|
+
screen.fill(BACKGROUND_COLOUR)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# FIXME: get the GIDs from the tileset
|
|
124
|
+
class TilesetGids(Enum):
|
|
125
|
+
GAP = 9
|
|
126
|
+
BRICK = 10
|
|
127
|
+
SAFE = 11
|
|
128
|
+
DIAMOND = 12
|
|
129
|
+
BLOB = 13
|
|
130
|
+
EARTH = 14
|
|
131
|
+
ROCK = 15
|
|
132
|
+
KEY = 16
|
|
133
|
+
WIN = 17
|
|
134
|
+
WIN_PLACE = 18
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
font_pixels = 8 * window_scale
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def text_to_screen(pos: Tuple[int, int]) -> Tuple[int, int]:
|
|
141
|
+
return (pos[0] * font_pixels, pos[1] * font_pixels)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def print_screen(pos: Tuple[int, int], msg: str, **kwargs: Any) -> None:
|
|
145
|
+
with importlib_resources.as_file(importlib_resources.files()) as path:
|
|
146
|
+
ptext.draw( # type: ignore[no-untyped-call]
|
|
147
|
+
msg,
|
|
148
|
+
text_to_screen(pos),
|
|
149
|
+
fontname=str(path / "acorn-mode-1.ttf"),
|
|
150
|
+
fontsize=font_pixels,
|
|
151
|
+
**kwargs,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def quit_game() -> NoReturn:
|
|
156
|
+
pygame.quit()
|
|
157
|
+
sys.exit()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def handle_quit_event() -> None:
|
|
161
|
+
if len(pygame.event.get(pygame.QUIT)) > 0:
|
|
162
|
+
quit_game()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def handle_global_keys(event: pygame.event.Event) -> None:
|
|
166
|
+
if event.key == pygame.K_F11:
|
|
167
|
+
pygame.display.toggle_fullscreen()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
FRAMES_PER_SECOND = 10
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def scale_surface(surface: pygame.Surface) -> pygame.Surface:
|
|
174
|
+
scaled_width = surface.get_width() * window_scale
|
|
175
|
+
scaled_height = surface.get_height() * window_scale
|
|
176
|
+
scaled_surface = pygame.Surface((scaled_width, scaled_height))
|
|
177
|
+
pygame.transform.scale(surface, (scaled_width, scaled_height), scaled_surface)
|
|
178
|
+
return scaled_surface
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class WincollGame:
|
|
182
|
+
def __init__(self, level: int = 1) -> None:
|
|
183
|
+
self.game_surface = pygame.Surface((window_pixels, window_pixels))
|
|
184
|
+
self.window_pos = ((screen.get_width() - scaled_pixels) // 2, 12 * window_scale)
|
|
185
|
+
self.quit = False
|
|
186
|
+
self.dead = False
|
|
187
|
+
self.falling = False
|
|
188
|
+
self.level = level
|
|
189
|
+
self.map_blocks: pytmx.TiledTileLayer
|
|
190
|
+
self.gids: dict[TilesetGids, int]
|
|
191
|
+
self.map_layer: pyscroll.BufferedRenderer
|
|
192
|
+
self.group: pyscroll.PyscrollGroup
|
|
193
|
+
self.hero: Win
|
|
194
|
+
self.diamonds: int
|
|
195
|
+
self.map_data: pyscroll.data.TiledMapData
|
|
196
|
+
self.joysticks: dict[int, pygame.joystick.JoystickType] = {}
|
|
197
|
+
|
|
198
|
+
def restart_level(self) -> None:
|
|
199
|
+
with importlib_resources.as_file(importlib_resources.files()) as path:
|
|
200
|
+
filename = path / "levels" / f"{str(self.level).zfill(2)}.tmx"
|
|
201
|
+
self.dead = False
|
|
202
|
+
|
|
203
|
+
tmx_data = pytmx.load_pygame(filename)
|
|
204
|
+
self.map_data = pyscroll.data.TiledMapData(tmx_data)
|
|
205
|
+
self.map_blocks = self.map_data.tmx.layers[0].data
|
|
206
|
+
|
|
207
|
+
# Dict mapping tileset GIDs to map gids
|
|
208
|
+
map_gids = self.map_data.tmx.gidmap
|
|
209
|
+
self.gids = {i: map_gids[i.value + 1][0][0] for i in TilesetGids}
|
|
210
|
+
|
|
211
|
+
w, h = window_blocks * block_pixels, window_blocks * block_pixels
|
|
212
|
+
self.map_layer = pyscroll.BufferedRenderer(self.map_data, (w, h))
|
|
213
|
+
self.group = pyscroll.PyscrollGroup(map_layer=self.map_layer)
|
|
214
|
+
|
|
215
|
+
self.hero = Win()
|
|
216
|
+
self.hero.position = pygame.Vector2(0, 0)
|
|
217
|
+
self.group.add(self.hero)
|
|
218
|
+
self.diamonds = 0
|
|
219
|
+
self.survey()
|
|
220
|
+
|
|
221
|
+
def start_level(self) -> None:
|
|
222
|
+
self.restart_level()
|
|
223
|
+
self.save_position()
|
|
224
|
+
|
|
225
|
+
def get(self, pos: pygame.Vector2) -> int:
|
|
226
|
+
# Anything outside the map is a brick
|
|
227
|
+
x, y = int(pos.x), int(pos.y)
|
|
228
|
+
if not ((0 <= x < level_size) and (0 <= y < level_size)):
|
|
229
|
+
return self.gids[TilesetGids.BRICK]
|
|
230
|
+
block = self.map_blocks[y][x]
|
|
231
|
+
if block == 0: # Missing tiles are gaps
|
|
232
|
+
block = self.gids[TilesetGids.GAP]
|
|
233
|
+
return block # type: ignore[no-any-return]
|
|
234
|
+
|
|
235
|
+
def set(self, pos: pygame.Vector2, gid: int) -> None:
|
|
236
|
+
self.map_blocks[int(pos.y)][int(pos.x)] = gid
|
|
237
|
+
# Update map
|
|
238
|
+
# FIXME: We invoke protected methods and access protected members.
|
|
239
|
+
ml = self.map_layer
|
|
240
|
+
rect = (int(pos.x), int(pos.y), 1, 1)
|
|
241
|
+
# pylint: disable-next=protected-access
|
|
242
|
+
ml._tile_queue = chain(ml._tile_queue, ml.data.get_tile_images_by_rect(rect))
|
|
243
|
+
# pylint: disable-next=protected-access
|
|
244
|
+
self.map_layer._flush_tile_queue(self.map_layer._buffer)
|
|
245
|
+
|
|
246
|
+
def save_position(self) -> None:
|
|
247
|
+
self.set(self.hero.position, self.gids[TilesetGids.WIN])
|
|
248
|
+
with open(SAVED_POSITION_FILE, "wb") as fh:
|
|
249
|
+
pickle.dump(self.map_blocks, fh)
|
|
250
|
+
|
|
251
|
+
def load_position(self) -> None:
|
|
252
|
+
if SAVED_POSITION_FILE.exists():
|
|
253
|
+
with open(SAVED_POSITION_FILE, "rb") as fh:
|
|
254
|
+
map_blocks = pickle.load(fh)
|
|
255
|
+
for row, blocks in enumerate(map_blocks):
|
|
256
|
+
for col, block in enumerate(blocks):
|
|
257
|
+
self.set(pygame.Vector2(col, row), block)
|
|
258
|
+
self.survey()
|
|
259
|
+
|
|
260
|
+
def survey(self) -> None:
|
|
261
|
+
"""Count diamonds on level and find start position."""
|
|
262
|
+
self.diamonds = 0
|
|
263
|
+
for row, blocks in enumerate(self.map_blocks):
|
|
264
|
+
for col, block in enumerate(blocks):
|
|
265
|
+
if block in (
|
|
266
|
+
self.gids[TilesetGids.DIAMOND],
|
|
267
|
+
self.gids[TilesetGids.SAFE],
|
|
268
|
+
):
|
|
269
|
+
self.diamonds += 1
|
|
270
|
+
elif block == self.gids[TilesetGids.WIN]:
|
|
271
|
+
self.hero.position = pygame.Vector2(col, row)
|
|
272
|
+
self.set(self.hero.position, self.gids[TilesetGids.WIN_PLACE])
|
|
273
|
+
|
|
274
|
+
def unlock(self) -> None:
|
|
275
|
+
"""Turn safes into diamonds"""
|
|
276
|
+
for row, blocks in enumerate(self.map_blocks):
|
|
277
|
+
for col, block in enumerate(blocks):
|
|
278
|
+
if block == self.gids[TilesetGids.SAFE]:
|
|
279
|
+
self.set(pygame.Vector2(col, row), self.gids[TilesetGids.DIAMOND])
|
|
280
|
+
UNLOCK_SOUND.play()
|
|
281
|
+
|
|
282
|
+
def draw(self) -> None:
|
|
283
|
+
self.group.center(self.hero.rect.center)
|
|
284
|
+
self.group.draw(self.game_surface)
|
|
285
|
+
|
|
286
|
+
def handle_joysticks(self) -> None:
|
|
287
|
+
for event in pygame.event.get(pygame.JOYDEVICEADDED):
|
|
288
|
+
joy = pygame.joystick.Joystick(event.device_index)
|
|
289
|
+
self.joysticks[joy.get_instance_id()] = joy
|
|
290
|
+
|
|
291
|
+
for event in pygame.event.get(pygame.JOYDEVICEREMOVED):
|
|
292
|
+
del self.joysticks[event.instance_id]
|
|
293
|
+
|
|
294
|
+
for joystick in self.joysticks.values():
|
|
295
|
+
axes = joystick.get_numaxes()
|
|
296
|
+
if axes >= 2: # Hopefully 0=L/R and 1=U/D
|
|
297
|
+
lr = joystick.get_axis(0)
|
|
298
|
+
if lr < -0.5:
|
|
299
|
+
self.hero.velocity = pygame.Vector2(-1, 0)
|
|
300
|
+
elif lr > 0.5:
|
|
301
|
+
self.hero.velocity = pygame.Vector2(1, 0)
|
|
302
|
+
ud = joystick.get_axis(1)
|
|
303
|
+
if ud < -0.5:
|
|
304
|
+
self.hero.velocity = pygame.Vector2(0, -1)
|
|
305
|
+
elif ud > 0.5:
|
|
306
|
+
self.hero.velocity = pygame.Vector2(0, 1)
|
|
307
|
+
|
|
308
|
+
def handle_input(self) -> None:
|
|
309
|
+
pressed = pygame.key.get_pressed()
|
|
310
|
+
self.hero.velocity = pygame.Vector2(0, 0)
|
|
311
|
+
if pressed[pygame.K_LEFT] or pressed[pygame.K_z]:
|
|
312
|
+
self.hero.velocity = pygame.Vector2(-1, 0)
|
|
313
|
+
elif pressed[pygame.K_RIGHT] or pressed[pygame.K_x]:
|
|
314
|
+
self.hero.velocity = pygame.Vector2(1, 0)
|
|
315
|
+
elif pressed[pygame.K_UP] or pressed[pygame.K_QUOTE]:
|
|
316
|
+
self.hero.velocity = pygame.Vector2(0, -1)
|
|
317
|
+
elif pressed[pygame.K_DOWN] or pressed[pygame.K_SLASH]:
|
|
318
|
+
self.hero.velocity = pygame.Vector2(0, 1)
|
|
319
|
+
elif pressed[pygame.K_l]:
|
|
320
|
+
flash_background()
|
|
321
|
+
self.load_position()
|
|
322
|
+
elif pressed[pygame.K_s]:
|
|
323
|
+
flash_background()
|
|
324
|
+
self.save_position()
|
|
325
|
+
elif pressed[pygame.K_r]:
|
|
326
|
+
flash_background()
|
|
327
|
+
self.restart_level()
|
|
328
|
+
elif pressed[pygame.K_q]:
|
|
329
|
+
self.quit = True
|
|
330
|
+
self.handle_joysticks()
|
|
331
|
+
|
|
332
|
+
def process_move(self) -> None:
|
|
333
|
+
newpos = self.hero.position + self.hero.velocity
|
|
334
|
+
block = self.get(newpos)
|
|
335
|
+
if block in (self.gids[TilesetGids.GAP], self.gids[TilesetGids.EARTH]):
|
|
336
|
+
pass
|
|
337
|
+
elif block == self.gids[TilesetGids.DIAMOND]:
|
|
338
|
+
COLLECT_SOUND.play()
|
|
339
|
+
self.diamonds -= 1
|
|
340
|
+
elif block == self.gids[TilesetGids.KEY]:
|
|
341
|
+
self.unlock()
|
|
342
|
+
elif block == self.gids[TilesetGids.ROCK]:
|
|
343
|
+
new_rockpos = self.hero.position + (self.hero.velocity * 2)
|
|
344
|
+
if (
|
|
345
|
+
self.hero.velocity.y == 0
|
|
346
|
+
and self.get(new_rockpos) == self.gids[TilesetGids.GAP]
|
|
347
|
+
):
|
|
348
|
+
self.set(new_rockpos, self.gids[TilesetGids.ROCK])
|
|
349
|
+
else:
|
|
350
|
+
self.hero.velocity = pygame.Vector2(0, 0)
|
|
351
|
+
else:
|
|
352
|
+
self.hero.velocity = pygame.Vector2(0, 0)
|
|
353
|
+
self.set(self.hero.position, self.gids[TilesetGids.GAP])
|
|
354
|
+
self.set(
|
|
355
|
+
self.hero.position + self.hero.velocity, self.gids[TilesetGids.WIN_PLACE]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def can_roll(self, pos: pygame.Vector2) -> bool:
|
|
359
|
+
side_block = self.get(pos)
|
|
360
|
+
side_below_block = self.get(pos + pygame.Vector2(0, 1))
|
|
361
|
+
return (
|
|
362
|
+
side_block == self.gids[TilesetGids.GAP]
|
|
363
|
+
and side_below_block == self.gids[TilesetGids.GAP]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def rockfall(self) -> None:
|
|
367
|
+
new_fall = False
|
|
368
|
+
|
|
369
|
+
def fall(oldpos: pygame.Vector2, newpos: pygame.Vector2) -> None:
|
|
370
|
+
block_below = self.get(newpos + pygame.Vector2(0, 1))
|
|
371
|
+
if block_below == self.gids[TilesetGids.WIN_PLACE]:
|
|
372
|
+
self.dead = True
|
|
373
|
+
self.set(oldpos, self.gids[TilesetGids.GAP])
|
|
374
|
+
self.set(newpos, self.gids[TilesetGids.ROCK])
|
|
375
|
+
nonlocal new_fall
|
|
376
|
+
if self.falling is False:
|
|
377
|
+
self.falling = True
|
|
378
|
+
SLIDE_SOUND.play(-1)
|
|
379
|
+
new_fall = True
|
|
380
|
+
|
|
381
|
+
for row, blocks in reversed(list(enumerate(self.map_blocks))):
|
|
382
|
+
for col, block in enumerate(blocks):
|
|
383
|
+
if block == self.gids[TilesetGids.ROCK]:
|
|
384
|
+
pos = pygame.Vector2(col, row)
|
|
385
|
+
pos_below = pos + pygame.Vector2(0, 1)
|
|
386
|
+
block_below = self.get(pos_below)
|
|
387
|
+
if block_below == self.gids[TilesetGids.GAP]:
|
|
388
|
+
fall(pos, pos_below)
|
|
389
|
+
elif block_below in (
|
|
390
|
+
self.gids[TilesetGids.ROCK],
|
|
391
|
+
self.gids[TilesetGids.KEY],
|
|
392
|
+
self.gids[TilesetGids.DIAMOND],
|
|
393
|
+
self.gids[TilesetGids.BLOB],
|
|
394
|
+
):
|
|
395
|
+
pos_left = pos + pygame.Vector2(-1, 0)
|
|
396
|
+
if self.can_roll(pos_left):
|
|
397
|
+
fall(pos, pos_left + pygame.Vector2(0, 1))
|
|
398
|
+
else:
|
|
399
|
+
pos_right = pos + pygame.Vector2(1, 0)
|
|
400
|
+
if self.can_roll(pos_right):
|
|
401
|
+
fall(pos, pos_right + pygame.Vector2(0, 1))
|
|
402
|
+
|
|
403
|
+
if new_fall is False:
|
|
404
|
+
self.falling = False
|
|
405
|
+
SLIDE_SOUND.stop()
|
|
406
|
+
|
|
407
|
+
def game_to_screen(self, x: int, y: int) -> Tuple[int, int]:
|
|
408
|
+
origin = self.map_layer.get_center_offset()
|
|
409
|
+
return (origin[0] + x * block_pixels, origin[1] + y * block_pixels)
|
|
410
|
+
|
|
411
|
+
def splurge(self, sprite: pygame.Surface) -> None:
|
|
412
|
+
"""Fill the game area with one sprite."""
|
|
413
|
+
surface = pygame.Surface((window_pixels, window_pixels)).convert()
|
|
414
|
+
for row in range(level_size):
|
|
415
|
+
for col in range(level_size):
|
|
416
|
+
surface.blit(sprite, self.game_to_screen(col, row))
|
|
417
|
+
self.show_screen(surface)
|
|
418
|
+
pygame.time.wait(3000)
|
|
419
|
+
|
|
420
|
+
def show_screen(self, surface: Optional[pygame.Surface] = None) -> None:
|
|
421
|
+
screen.blit(scale_surface(surface or self.game_surface), self.window_pos)
|
|
422
|
+
pygame.display.flip()
|
|
423
|
+
screen.fill(BACKGROUND_COLOUR)
|
|
424
|
+
fade_background()
|
|
425
|
+
|
|
426
|
+
def show_status(self) -> None:
|
|
427
|
+
print_screen(
|
|
428
|
+
(0, 0),
|
|
429
|
+
_("Level {}:").format(self.level)
|
|
430
|
+
+ " "
|
|
431
|
+
+ self.map_data.tmx.properties["Title"],
|
|
432
|
+
width=screen.get_width(),
|
|
433
|
+
align="center",
|
|
434
|
+
color="grey",
|
|
435
|
+
)
|
|
436
|
+
screen.blit(DIAMOND_IMAGE, (2 * font_pixels, int(1.5 * font_pixels)))
|
|
437
|
+
print_screen(
|
|
438
|
+
(0, 3), str(self.diamonds), width=self.window_pos[0], align="center"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def run(self) -> None:
|
|
442
|
+
clock = pygame.time.Clock()
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
while self.level <= levels:
|
|
446
|
+
self.start_level()
|
|
447
|
+
self.show_status()
|
|
448
|
+
self.show_screen()
|
|
449
|
+
while self.diamonds > 0:
|
|
450
|
+
self.load_position()
|
|
451
|
+
while not self.dead and self.diamonds > 0:
|
|
452
|
+
clock.tick(FRAMES_PER_SECOND)
|
|
453
|
+
self.hero.velocity = pygame.Vector2(0, 0)
|
|
454
|
+
handle_quit_event()
|
|
455
|
+
for event in pygame.event.get(pygame.KEYDOWN):
|
|
456
|
+
handle_global_keys(event)
|
|
457
|
+
self.handle_input()
|
|
458
|
+
if self.quit:
|
|
459
|
+
self.quit = False
|
|
460
|
+
return
|
|
461
|
+
self.process_move()
|
|
462
|
+
self.rockfall()
|
|
463
|
+
subframes = 4
|
|
464
|
+
for _subframe in range(subframes):
|
|
465
|
+
self.group.update(1 / subframes)
|
|
466
|
+
self.draw()
|
|
467
|
+
self.show_status()
|
|
468
|
+
self.show_screen()
|
|
469
|
+
pygame.time.wait(1000 // FRAMES_PER_SECOND // subframes)
|
|
470
|
+
if self.dead:
|
|
471
|
+
SLIDE_SOUND.stop()
|
|
472
|
+
SPLAT_SOUND.play()
|
|
473
|
+
self.game_surface.blit(
|
|
474
|
+
SPLAT_IMAGE,
|
|
475
|
+
self.game_to_screen(
|
|
476
|
+
int(self.hero.position.x), int(self.hero.position.y)
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
self.show_status()
|
|
480
|
+
self.show_screen()
|
|
481
|
+
pygame.time.wait(1000)
|
|
482
|
+
self.dead = False
|
|
483
|
+
self.level += 1
|
|
484
|
+
self.splurge(Win().image)
|
|
485
|
+
finally:
|
|
486
|
+
SLIDE_SOUND.stop()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class Win(pygame.sprite.Sprite): # pylint: disable=too-few-public-methods
|
|
490
|
+
def __init__(self) -> None:
|
|
491
|
+
pygame.sprite.Sprite.__init__(self)
|
|
492
|
+
self.image = load_image("levels/Win.png")
|
|
493
|
+
self.velocity = pygame.Vector2(0, 0)
|
|
494
|
+
self.position = pygame.Vector2(0, 0)
|
|
495
|
+
self.rect = self.image.get_rect()
|
|
496
|
+
|
|
497
|
+
def update(self, dt: float) -> None:
|
|
498
|
+
self.position += self.velocity * dt
|
|
499
|
+
screen_pos = self.position * block_pixels
|
|
500
|
+
self.rect.topleft = (int(screen_pos.x), int(screen_pos.y))
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def clear_keys() -> None:
|
|
504
|
+
for _event in pygame.event.get(pygame.KEYDOWN):
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def get_key() -> int:
|
|
509
|
+
"""Return first key press."""
|
|
510
|
+
while True:
|
|
511
|
+
handle_quit_event()
|
|
512
|
+
for event in pygame.event.get(pygame.KEYDOWN):
|
|
513
|
+
if event.key == pygame.K_ESCAPE:
|
|
514
|
+
quit_game()
|
|
515
|
+
else:
|
|
516
|
+
handle_global_keys(event)
|
|
517
|
+
key: int = event.key
|
|
518
|
+
return key
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
DIGIT_KEYS = {
|
|
522
|
+
pygame.K_0: 0,
|
|
523
|
+
pygame.K_1: 1,
|
|
524
|
+
pygame.K_2: 2,
|
|
525
|
+
pygame.K_3: 3,
|
|
526
|
+
pygame.K_4: 4,
|
|
527
|
+
pygame.K_5: 5,
|
|
528
|
+
pygame.K_6: 6,
|
|
529
|
+
pygame.K_7: 7,
|
|
530
|
+
pygame.K_8: 8,
|
|
531
|
+
pygame.K_9: 9,
|
|
532
|
+
pygame.K_KP_0: 0,
|
|
533
|
+
pygame.K_KP_1: 1,
|
|
534
|
+
pygame.K_KP_2: 2,
|
|
535
|
+
pygame.K_KP_3: 3,
|
|
536
|
+
pygame.K_KP_4: 4,
|
|
537
|
+
pygame.K_KP_5: 5,
|
|
538
|
+
pygame.K_KP_6: 6,
|
|
539
|
+
pygame.K_KP_7: 7,
|
|
540
|
+
pygame.K_KP_8: 8,
|
|
541
|
+
pygame.K_KP_9: 9,
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def instructions() -> int:
|
|
546
|
+
"""Show instructions and choose start level."""
|
|
547
|
+
clear_keys()
|
|
548
|
+
level = 0
|
|
549
|
+
clock = pygame.time.Clock()
|
|
550
|
+
instructions = _(
|
|
551
|
+
"""\
|
|
552
|
+
Collect all the diamonds on each level.
|
|
553
|
+
Get a key to turn safes into diamonds.
|
|
554
|
+
Avoid falling rocks!
|
|
555
|
+
|
|
556
|
+
Z/X - Left/Right '/? - Up/Down
|
|
557
|
+
or use the cursor keys to move
|
|
558
|
+
S/L - Save/load position
|
|
559
|
+
R - Restart level Q - Quit game
|
|
560
|
+
F11 - toggle full screen
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
(choose with movement keys and digits)
|
|
564
|
+
|
|
565
|
+
Press the space bar to play!
|
|
566
|
+
"""
|
|
567
|
+
)
|
|
568
|
+
instructions_y = 14
|
|
569
|
+
start_level_y = (
|
|
570
|
+
instructions_y + len(instructions.split("\n\n\n")[0].split("\n")) + 1
|
|
571
|
+
)
|
|
572
|
+
while True:
|
|
573
|
+
reinit_screen()
|
|
574
|
+
screen.blit(
|
|
575
|
+
scale_surface(TITLE_IMAGE.convert()),
|
|
576
|
+
(110 * window_scale, 20 * window_scale),
|
|
577
|
+
)
|
|
578
|
+
print_screen((0, 14), instructions, color="grey")
|
|
579
|
+
print_screen(
|
|
580
|
+
(0, start_level_y),
|
|
581
|
+
_("Start level: {}/{}").format(1 if level == 0 else level, levels),
|
|
582
|
+
width=screen.get_width(),
|
|
583
|
+
align="center",
|
|
584
|
+
)
|
|
585
|
+
pygame.display.flip()
|
|
586
|
+
key = get_key()
|
|
587
|
+
clock.tick(FRAMES_PER_SECOND)
|
|
588
|
+
if key == pygame.K_SPACE:
|
|
589
|
+
break
|
|
590
|
+
if key in (pygame.K_z, pygame.K_LEFT, pygame.K_SLASH, pygame.K_DOWN):
|
|
591
|
+
level = max(1, level - 1)
|
|
592
|
+
elif key in (pygame.K_x, pygame.K_RIGHT, pygame.K_QUOTE, pygame.K_UP):
|
|
593
|
+
level = min(levels, level + 1)
|
|
594
|
+
elif key in DIGIT_KEYS:
|
|
595
|
+
level = min(levels, level * 10 + DIGIT_KEYS[key])
|
|
596
|
+
else:
|
|
597
|
+
level = 0
|
|
598
|
+
return max(min(level, levels), 1)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def main(argv: List[str] = sys.argv[1:]) -> None:
|
|
602
|
+
# Command-line arguments
|
|
603
|
+
parser = argparse.ArgumentParser(
|
|
604
|
+
description=_(
|
|
605
|
+
"Collect all the diamonds while digging through earth dodging rocks."
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
parser.add_argument(
|
|
609
|
+
"-V",
|
|
610
|
+
"--version",
|
|
611
|
+
action="version",
|
|
612
|
+
version=_("%(prog)s {} ({}) by Reuben Thomas <rrt@sc3d.org>").format(
|
|
613
|
+
VERSION, datetime(2024, 12, 16).strftime("%d %b %Y")
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
warnings.showwarning = simple_warning(parser.prog)
|
|
617
|
+
parser.parse_args(argv)
|
|
618
|
+
|
|
619
|
+
pygame.init()
|
|
620
|
+
pygame.font.init()
|
|
621
|
+
pygame.key.set_repeat()
|
|
622
|
+
pygame.joystick.init()
|
|
623
|
+
pygame.display.set_caption("WinColl")
|
|
624
|
+
init_screen()
|
|
625
|
+
|
|
626
|
+
global COLLECT_SOUND, SLIDE_SOUND, UNLOCK_SOUND, SPLAT_SOUND
|
|
627
|
+
with importlib_resources.as_file(importlib_resources.files()) as path:
|
|
628
|
+
COLLECT_SOUND = pygame.mixer.Sound(path / "Collect.wav")
|
|
629
|
+
SLIDE_SOUND = pygame.mixer.Sound(path / "Slide.wav")
|
|
630
|
+
UNLOCK_SOUND = pygame.mixer.Sound(path / "Unlock.wav")
|
|
631
|
+
SPLAT_SOUND = pygame.mixer.Sound(path / "Splat.wav")
|
|
632
|
+
|
|
633
|
+
try:
|
|
634
|
+
while True:
|
|
635
|
+
level = instructions()
|
|
636
|
+
game = WincollGame(level)
|
|
637
|
+
game.run()
|
|
638
|
+
except KeyboardInterrupt:
|
|
639
|
+
quit_game()
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
if __name__ == "__main__":
|
|
643
|
+
main()
|
wincoll/__main__.py
ADDED
wincoll/acorn-mode-1.ttf
ADDED
|
Binary file
|
wincoll/diamond.png
ADDED
|
Binary file
|