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/__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
@@ -0,0 +1,7 @@
1
+ import re
2
+ import sys
3
+
4
+ from wincoll import main
5
+
6
+ sys.argv[0] = re.sub(r"__main__.py$", "wincoll", sys.argv[0])
7
+ main()
Binary file
wincoll/diamond.png ADDED
Binary file