Format code with black
This commit is contained in:
107
game/game.py
107
game/game.py
@@ -9,8 +9,8 @@ from .assets import AssetStore
|
||||
from .level import Level
|
||||
from .player import Player, InputState
|
||||
|
||||
HUD_H = 46 # height of the status bar above the play area
|
||||
MIN_W = 480 # keep the window wide enough for the HUD text
|
||||
HUD_H = 46 # height of the status bar above the play area
|
||||
MIN_W = 480 # keep the window wide enough for the HUD text
|
||||
|
||||
|
||||
class Game:
|
||||
@@ -18,7 +18,7 @@ class Game:
|
||||
pygame.init()
|
||||
pygame.display.set_caption(S.CAPTION)
|
||||
self.level_paths = level_paths
|
||||
self.force_debug = debug # --debug: turn debug view on for every level
|
||||
self.force_debug = debug # --debug: turn debug view on for every level
|
||||
self.assets = AssetStore(assets_dir)
|
||||
self.clock = pygame.time.Clock()
|
||||
self.hud_font = pygame.font.SysFont("consolas,menlo,monospace", 22, bold=True)
|
||||
@@ -26,12 +26,12 @@ class Game:
|
||||
|
||||
self.index = 0
|
||||
self.running = True
|
||||
self.state = "playing" # playing | dying | won_level | won_all
|
||||
self.death_timer = 0.0 # counts down during the "dying" pause
|
||||
self.death_rect = None # where to draw the corpse
|
||||
self.deaths = 0 # total deaths this session
|
||||
self.level_deaths = 0 # deaths on the current level (resets per level)
|
||||
self._pending_reload = False # F5: reload the level file on next respawn
|
||||
self.state = "playing" # playing | dying | won_level | won_all
|
||||
self.death_timer = 0.0 # counts down during the "dying" pause
|
||||
self.death_rect = None # where to draw the corpse
|
||||
self.deaths = 0 # total deaths this session
|
||||
self.level_deaths = 0 # deaths on the current level (resets per level)
|
||||
self._pending_reload = False # F5: reload the level file on next respawn
|
||||
|
||||
# Size the window ONCE to fit the largest level, then never resize it —
|
||||
# calling set_mode again mid-session recreates the window (it flickers /
|
||||
@@ -62,7 +62,7 @@ class Game:
|
||||
def _load_current(self):
|
||||
self.level = Level(self.level_paths[self.index])
|
||||
if self.force_debug:
|
||||
self.level.debug = True # CLI flag overrides the per-level setting
|
||||
self.level.debug = True # CLI flag overrides the per-level setting
|
||||
self.player = Player(self.level)
|
||||
self.battery = self.level.battery
|
||||
# In debug, grow the play surface by a margin and shift all drawing into
|
||||
@@ -78,7 +78,7 @@ class Game:
|
||||
self.state = "playing"
|
||||
self.death_timer = 0.0
|
||||
self.death_rect = None
|
||||
self.level_deaths = 0 # fresh count for the level we just loaded
|
||||
self.level_deaths = 0 # fresh count for the level we just loaded
|
||||
|
||||
def _start_death(self):
|
||||
# Begin the death pause: freeze everything, leave the corpse on screen.
|
||||
@@ -104,7 +104,7 @@ class Game:
|
||||
try:
|
||||
self._load_current()
|
||||
self.level_deaths = kept
|
||||
except Exception as ex: # bad edit -> don't crash
|
||||
except Exception as ex: # bad edit -> don't crash
|
||||
print(f"[reload] {self.level_paths[self.index]}: {ex}")
|
||||
self.player.respawn()
|
||||
self.level.reset()
|
||||
@@ -112,7 +112,7 @@ class Game:
|
||||
else:
|
||||
self.player.respawn()
|
||||
self.level.reset()
|
||||
self.battery = self.level.battery # phone gets plugged back in at start
|
||||
self.battery = self.level.battery # phone gets plugged back in at start
|
||||
self.state = "playing"
|
||||
|
||||
def _reload_level(self):
|
||||
@@ -129,7 +129,7 @@ class Game:
|
||||
self.state = "charging"
|
||||
self.charge_timer = 0.0
|
||||
tf = max(0.0, self.battery / self.level.battery) if self.level.battery else 0.0
|
||||
self.charge_from = tf * (self.level.battery_pct / 100.0) # near-empty start
|
||||
self.charge_from = tf * (self.level.battery_pct / 100.0) # near-empty start
|
||||
self._final = self.index + 1 >= len(self.level_paths)
|
||||
|
||||
def _advance(self):
|
||||
@@ -157,8 +157,8 @@ class Game:
|
||||
if self.fade_timer < S.FADE_TIME:
|
||||
return
|
||||
if self.fade_phase == "out":
|
||||
self.fade_action() # swap levels while the screen is black
|
||||
self.state = "fading" # _load_current() flips to "playing"; undo it
|
||||
self.fade_action() # swap levels while the screen is black
|
||||
self.state = "fading" # _load_current() flips to "playing"; undo it
|
||||
self.fade_phase = "in"
|
||||
self.fade_timer = 0.0
|
||||
else:
|
||||
@@ -195,7 +195,7 @@ class Game:
|
||||
def run(self):
|
||||
while self.running:
|
||||
dt = self.clock.tick(S.FPS) / 1000.0
|
||||
dt = min(dt, 1 / 30) # clamp to avoid tunneling on lag spikes
|
||||
dt = min(dt, 1 / 30) # clamp to avoid tunneling on lag spikes
|
||||
inp = self._poll_events()
|
||||
|
||||
if self.state == "playing":
|
||||
@@ -224,8 +224,8 @@ class Game:
|
||||
# death conditions
|
||||
pr = self.player.rect
|
||||
died = self.battery <= 0
|
||||
died = died or pr.top > self.level.height + 160 # fell out of the world
|
||||
died = died or self.player.crushed # pinched by a moving block
|
||||
died = died or pr.top > self.level.height + 160 # fell out of the world
|
||||
died = died or self.player.crushed # pinched by a moving block
|
||||
if not died:
|
||||
for hz in self.level.hazard_rects():
|
||||
if pr.colliderect(hz):
|
||||
@@ -257,7 +257,9 @@ class Game:
|
||||
if self.state == "dying" and self.death_rect is not None:
|
||||
# The traps stay drawn in their moment-of-death state; the player is
|
||||
# replaced by a corpse sprite where they fell.
|
||||
sprite = self.assets.get("player_dead", self.death_rect.w, self.death_rect.h)
|
||||
sprite = self.assets.get(
|
||||
"player_dead", self.death_rect.w, self.death_rect.h
|
||||
)
|
||||
self.world.blit(sprite, self.death_rect.move(self.level.render_offset))
|
||||
else:
|
||||
self.player.draw(self.world, self.assets)
|
||||
@@ -278,13 +280,17 @@ class Game:
|
||||
|
||||
if self.state == "won_level":
|
||||
plural = "death" if self.level_deaths == 1 else "deaths"
|
||||
self._draw_win_splash("LEVEL COMPLETE — phone charged!",
|
||||
f"{self.level_deaths} {plural} here · {self.deaths} total · ENTER for the next level")
|
||||
self._draw_win_splash(
|
||||
"LEVEL COMPLETE — phone charged!",
|
||||
f"{self.level_deaths} {plural} here · {self.deaths} total · ENTER for the next level",
|
||||
)
|
||||
elif self.state == "won_all":
|
||||
self._draw_win_splash("YOU MADE IT! Phone fully charged.",
|
||||
f"All levels cleared with {self.deaths} deaths. ENTER to replay")
|
||||
self._draw_win_splash(
|
||||
"YOU MADE IT! Phone fully charged.",
|
||||
f"All levels cleared with {self.deaths} deaths. ENTER to replay",
|
||||
)
|
||||
|
||||
if self.state == "fading": # fade-in only; fade-out returned early above
|
||||
if self.state == "fading": # fade-in only; fade-out returned early above
|
||||
p = min(1.0, self.fade_timer / S.FADE_TIME)
|
||||
overlay = pygame.Surface(self.screen.get_size())
|
||||
overlay.fill((0, 0, 0))
|
||||
@@ -300,16 +306,22 @@ class Game:
|
||||
r = max(2, rect.h // 6)
|
||||
inset = max(2, rect.h // 10)
|
||||
pygame.draw.rect(surf, (60, 60, 72), rect, border_radius=r)
|
||||
pygame.draw.rect(surf, (28, 30, 40),
|
||||
rect.inflate(-inset, -inset), border_radius=r)
|
||||
pygame.draw.rect(
|
||||
surf, (28, 30, 40), rect.inflate(-inset, -inset), border_radius=r
|
||||
)
|
||||
fw = int((rect.w - inset * 2) * max(0.0, min(1.0, fill_frac)))
|
||||
if fill_frac > 0:
|
||||
fw = max(inset, fw)
|
||||
pygame.draw.rect(surf, col, (rect.x + inset, rect.y + inset, fw, rect.h - inset * 2),
|
||||
border_radius=r)
|
||||
pygame.draw.rect(
|
||||
surf,
|
||||
col,
|
||||
(rect.x + inset, rect.y + inset, fw, rect.h - inset * 2),
|
||||
border_radius=r,
|
||||
)
|
||||
nub_w, nub_h = max(3, rect.h // 4), rect.h // 2
|
||||
pygame.draw.rect(surf, (60, 60, 72),
|
||||
(rect.right, rect.centery - nub_h // 2, nub_w, nub_h))
|
||||
pygame.draw.rect(
|
||||
surf, (60, 60, 72), (rect.right, rect.centery - nub_h // 2, nub_w, nub_h)
|
||||
)
|
||||
|
||||
def _draw_hud(self, hide_battery=False):
|
||||
w = self.screen.get_width()
|
||||
@@ -322,17 +334,24 @@ class Game:
|
||||
bar_w, bar_h = 180, 20
|
||||
bx, by = 12, (HUD_H - bar_h) // 2
|
||||
if not hide_battery:
|
||||
time_frac = max(0.0, self.battery / self.level.battery) if self.level.battery else 0.0
|
||||
time_frac = (
|
||||
max(0.0, self.battery / self.level.battery)
|
||||
if self.level.battery
|
||||
else 0.0
|
||||
)
|
||||
visual = time_frac * (self.level.battery_pct / 100.0)
|
||||
col = (240, 170, 60) if time_frac > 0.25 else (240, 80, 80)
|
||||
self._draw_battery(self.screen, pygame.Rect(bx, by, bar_w, bar_h), visual, col)
|
||||
self._draw_battery(
|
||||
self.screen, pygame.Rect(bx, by, bar_w, bar_h), visual, col
|
||||
)
|
||||
|
||||
secs = max(0.0, self.battery)
|
||||
label = self.hud_font.render(f"{secs:4.1f}s", True, S.COLOR_HUD)
|
||||
self.screen.blit(label, (bx + bar_w + 16, by - 1))
|
||||
|
||||
deaths = self.hud_font.render(
|
||||
f"deaths {self.level_deaths} / {self.deaths} total", True, S.COLOR_HUD)
|
||||
f"deaths {self.level_deaths} / {self.deaths} total", True, S.COLOR_HUD
|
||||
)
|
||||
self.screen.blit(deaths, (w - deaths.get_width() - 12, by - 1))
|
||||
|
||||
name = self.hud_font.render(self.level.name, True, S.COLOR_HUD)
|
||||
@@ -355,8 +374,8 @@ class Game:
|
||||
it fills to 100%."""
|
||||
W, H = self.screen.get_size()
|
||||
t = min(1.0, self.charge_timer / S.CHARGE_TIME)
|
||||
move_p = 1 - (1 - min(1.0, t / 0.4)) ** 3 # ease-out; centred by 40%
|
||||
fill_p = max(0.0, min(1.0, (t - 0.2) / 0.6)) # fill from 20%..80%
|
||||
move_p = 1 - (1 - min(1.0, t / 0.4)) ** 3 # ease-out; centred by 40%
|
||||
fill_p = max(0.0, min(1.0, (t - 0.2) / 0.6)) # fill from 20%..80%
|
||||
fill = self.charge_from + (1.0 - self.charge_from) * fill_p
|
||||
|
||||
dim = pygame.Surface((W, H), pygame.SRCALPHA)
|
||||
@@ -367,9 +386,12 @@ class Game:
|
||||
home = pygame.Vector2(12 + bar_w / 2, (HUD_H - bar_h) // 2 + bar_h / 2)
|
||||
target = self._hero_battery()
|
||||
pos = home.lerp(pygame.Vector2(target.center), move_p)
|
||||
rect = pygame.Rect(0, 0,
|
||||
round(bar_w + (target.w - bar_w) * move_p),
|
||||
round(bar_h + (target.h - bar_h) * move_p))
|
||||
rect = pygame.Rect(
|
||||
0,
|
||||
0,
|
||||
round(bar_w + (target.w - bar_w) * move_p),
|
||||
round(bar_h + (target.h - bar_h) * move_p),
|
||||
)
|
||||
rect.center = (round(pos.x), round(pos.y))
|
||||
self._draw_battery(self.screen, rect, fill, (90, 220, 120))
|
||||
|
||||
@@ -391,7 +413,10 @@ class Game:
|
||||
s = self.hud_font.render(subtitle, True, S.COLOR_HUD)
|
||||
self.screen.blit(s, s.get_rect(center=(W // 2, rect.bottom + 34)))
|
||||
|
||||
|
||||
def discover_levels(levels_dir):
|
||||
paths = sorted(glob.glob(os.path.join(levels_dir, "*.yaml")) +
|
||||
glob.glob(os.path.join(levels_dir, "*.yml")))
|
||||
paths = sorted(
|
||||
glob.glob(os.path.join(levels_dir, "*.yaml"))
|
||||
+ glob.glob(os.path.join(levels_dir, "*.yml"))
|
||||
)
|
||||
return paths
|
||||
|
||||
Reference in New Issue
Block a user