Portrait 360x480 table with regular-sized flippers
Rotate the playfield to portrait like a real pinball machine: - canvas 480x360 landscape -> 360x480 portrait; playfield 308px wide, ~390px tall, with HUD, title/game-over screens and HUD re-laid out for the narrow format (score/best top row, balls/stars/fever below) - flippers reduced to a regular ~quarter-width size (82px) pivoting at the lower corners, tips meeting over a wide centre drain - table elements re-placed for the taller field: 3 star lanes up top, candy bumpers mid-field, slingshots and floor-level outlane guard posts at the bottom; data-checked with the autoplay bot (~350 bumper hits / 30k frames, centre+side drains, no stuck balls) - probes & autoplay policy updated for the portrait geometry; all 13 pixel-probes pass
This commit is contained in:
@@ -9,7 +9,7 @@ import pygame
|
||||
from physics import (CV_W, CV_H, PLAY_L, PLAY_R, PLAY_T, PLAY_B, BALL_R,
|
||||
CENTER_HOLE, OUTLANE_L, OUTLANE_R, DRAIN_Y, LAUNCH_DIR,
|
||||
LAUNCH_MIN, LAUNCH_MAX, Table, FLIP_PIVOT_L, FLIP_PIVOT_R,
|
||||
FLIP_RADIUS, FLIP_LEN, flipper_tip)
|
||||
FLIP_RADIUS, FLIP_LEN, flipper_tip, BUMPERS)
|
||||
import font as pix
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -362,9 +362,9 @@ class Game:
|
||||
t = y / CV_H
|
||||
col = tuple(int(a + (b - a) * t) for a, b in zip(C["bg_top"], C["bg_bot"]))
|
||||
pygame.draw.line(bg, col, (0, y), (CV_W, y))
|
||||
# twinkle stars & planets for the background
|
||||
# twinkle stars & planets around the table, drawn over the HUD & margins
|
||||
for sx, sy, tw, ph in self.stars_bg:
|
||||
if 0 < sx < 20 or 460 < sx < CV_W or sy < 46:
|
||||
if sx < PLAY_L - 6 or sx > PLAY_R + 6 or sy < 50:
|
||||
pygame.draw.rect(bg, (230, 225, 255), (sx, sy, 1, 1))
|
||||
self.bg = bg
|
||||
|
||||
@@ -382,12 +382,11 @@ class Game:
|
||||
PLAY_R - PLAY_L - 8, PLAY_B - PLAY_T - 8), border_radius=5)
|
||||
# candy dot sparkle inside the field
|
||||
r = self.rng
|
||||
for _ in range(46):
|
||||
for _ in range(60):
|
||||
dx = r.randrange(int(PLAY_L) + 12, int(PLAY_R) - 12)
|
||||
dy = r.randrange(int(PLAY_T) + 14, int(PLAY_B) - 12)
|
||||
if any((dx - b["pos"][0]) ** 2 + (dy - b["pos"][1]) ** 2 < (b["r"] + 4) ** 2
|
||||
for b in ({"pos": (150, 180), "r": 23}, {"pos": (330, 180), "r": 23},
|
||||
{"pos": (240, 240), "r": 23})):
|
||||
for b in BUMPERS):
|
||||
continue
|
||||
col = PASTELS[r.randrange(len(PASTELS))]
|
||||
field.fill(tuple(c // 3 for c in col), (dx, dy, 2, 2))
|
||||
@@ -412,8 +411,11 @@ class Game:
|
||||
hud.fill(C["hud"])
|
||||
pygame.draw.line(hud, C["rail_dark"], (0, 50), (CV_W, 50), 2)
|
||||
pygame.draw.line(hud, C["hud_line"], (0, 47), (CV_W, 47), 1)
|
||||
pygame.draw.rect(hud, (255, 255, 255), (0, 0, 8, 52))
|
||||
pygame.draw.rect(hud, (255, 255, 255), (CV_W - 8, 0, 8, 52))
|
||||
# static labels
|
||||
pix.draw_text(hud, 10, 4, "SCORE", C["dim"], 1)
|
||||
pix.draw_text(hud, CV_W - 10, 4, "BEST", C["dim"], 1, align="right")
|
||||
pix.draw_text(hud, 10, 33, "BALLS", C["dim"], 1)
|
||||
pix.draw_text(hud, 120, 33, "STARS", C["dim"], 1)
|
||||
self.hud = hud
|
||||
|
||||
# ------------------------------------------------------------------ game
|
||||
@@ -471,8 +473,8 @@ class Game:
|
||||
self.sfx.play("fever")
|
||||
self.cat_say("CANDY FEVER!", 90, "star")
|
||||
self.shake = 3
|
||||
self.add_msg("CANDY FEVER!", CV_W // 2, 150, C["gold"], scale=2)
|
||||
self.add_msg("DOUBLE SCORE x2", CV_W // 2, 166, C["pink"], scale=1)
|
||||
self.add_msg("CANDY FEVER!", CV_W // 2, 210, C["gold"], scale=2)
|
||||
self.add_msg("DOUBLE SCORE x2", CV_W // 2, 226, C["pink"], scale=1)
|
||||
for i in range(30):
|
||||
self.particles.append(dict(
|
||||
x=self.rng.uniform(PLAY_L + 10, PLAY_R - 10), y=self.rng.uniform(60, 120),
|
||||
@@ -645,7 +647,7 @@ class Game:
|
||||
self.lives += 1
|
||||
self.sfx.play("extra")
|
||||
self.cat_say("EXTRA BALL!", 90, "star")
|
||||
self.add_msg("EXTRA BALL!", CV_W // 2, 120, C["pink"], scale=2)
|
||||
self.add_msg("EXTRA BALL!", CV_W // 2, 200, C["pink"], scale=2)
|
||||
self.next_extra += 30000
|
||||
|
||||
def add_msg(self, text, x, y, color, scale=1):
|
||||
@@ -741,7 +743,7 @@ class Game:
|
||||
# combo indicator
|
||||
if self.combo >= 2 and self.pending == "live":
|
||||
col = (255, 140, 90) if self.combo >= 5 else C["gold"]
|
||||
pix.draw_text(c, CV_W // 2, 56, "COMBO x%d!" % self.combo, col, 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 68, "COMBO x%d!" % self.combo, col, 1, align="center")
|
||||
|
||||
# particles
|
||||
for p in self.particles:
|
||||
@@ -772,21 +774,21 @@ class Game:
|
||||
# pending banners
|
||||
if self.pending == "ball_banner" and self.pending_t > 40:
|
||||
self._dim_center(c)
|
||||
pix.draw_text(c, CV_W // 2, 140, "BALL %d" % self.ball_no, C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 170, "GET READY...", C["pink"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 255, "BALL %d" % self.ball_no, C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 300, "GET READY...", C["pink"], 1, align="center")
|
||||
if self.pending == "drain_pause":
|
||||
self._dim_center(c)
|
||||
pix.draw_text(c, CV_W // 2, 150, "LOST THE BALL!", C["pink"], 2, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 285, "LOST THE BALL!", C["pink"], 2, align="center")
|
||||
if self.pending == "serve" and not t.ball_live:
|
||||
pix.draw_text(c, CV_W // 2, 300, "HOLD SPACE / UP TO CHARGE", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 372, "HOLD SPACE / UP TO CHARGE", C["dim"], 1, align="center")
|
||||
|
||||
# HUD
|
||||
self._render_hud(c)
|
||||
|
||||
def _dim_center(self, c):
|
||||
self.overlay.fill((0, 0, 0, 0))
|
||||
pygame.draw.rect(self.overlay, (12, 8, 40, 160),
|
||||
(CV_W // 2 - 100, 110, 200, 90), border_radius=8)
|
||||
pygame.draw.rect(self.overlay, (12, 8, 40, 170),
|
||||
(CV_W // 2 - 100, 230, 200, 120), border_radius=10)
|
||||
c.blit(self.overlay, (0, 0))
|
||||
|
||||
def _draw_bubble(self, c):
|
||||
@@ -801,29 +803,28 @@ class Game:
|
||||
|
||||
def _render_hud(self, c):
|
||||
c.blit(self.hud, (0, 0))
|
||||
pix.draw_text(c, 14, 4, "SCORE", C["dim"], 1)
|
||||
pix.draw_text(c, 14, 13, "{:,}".format(self.score), C["white"], 2)
|
||||
pix.draw_text(c, 170, 4, "BEST", C["dim"], 1)
|
||||
pix.draw_text(c, 10, 13, "{:,}".format(self.score), C["white"], 2)
|
||||
best = max(self.hi, self.score)
|
||||
pix.draw_text(c, 170, 13, "{:,}".format(best), C["gold"], 2)
|
||||
# lives hearts
|
||||
for i in range(self.lives):
|
||||
draw_heart(c, 336 + i * 14, 14, 2, C["heart"])
|
||||
pix.draw_text(c, 336, 30, "LIVES", C["dim"], 1)
|
||||
# stars progress
|
||||
pix.draw_text(c, 14, 32, "STARS", C["dim"], 1)
|
||||
pix.draw_text(c, CV_W - 10, 13, "{:,}".format(best), C["gold"], 2, align="right")
|
||||
# lives hearts (cap the icon row, spill into a +N text)
|
||||
hearts = self.lives
|
||||
for i in range(min(hearts, 5)):
|
||||
draw_heart(c, 10 + i * 14, 42, 2, C["heart"])
|
||||
if hearts > 5:
|
||||
pix.draw_text(c, 10 + 5 * 14 + 2, 43, "+%d" % (hearts - 5), C["pink"], 1)
|
||||
# star progress
|
||||
for i, st in enumerate(self.table.stars):
|
||||
x = 52 + i * 18
|
||||
draw_star(c, x, 37, 5 if st["lit"] else 4,
|
||||
x = 120 + i * 16
|
||||
draw_star(c, x + 4, 42, 5 if st["lit"] else 4,
|
||||
C["gold"] if st["lit"] else (90, 78, 150))
|
||||
# fever indicator + bar
|
||||
if self.fever > 0:
|
||||
frac = max(0.0, self.fever / (60 * 20))
|
||||
pygame.draw.rect(c, (60, 46, 110), (260, 30, 70, 6))
|
||||
pygame.draw.rect(c, (255, 150, 200), (260, 30, int(70 * frac), 6))
|
||||
pix.draw_text(c, 260, 38, "FEVER x2", C["gold"], 1)
|
||||
pix.draw_text(c, 205, 33, "FEVER", C["gold"], 1)
|
||||
pygame.draw.rect(c, (60, 46, 110), (205, 44, 90, 5))
|
||||
pygame.draw.rect(c, (255, 150, 200), (205, 44, int(90 * frac), 5))
|
||||
if self.paused:
|
||||
pix.draw_text(c, CV_W - 40, 40, "PAUSED", C["dim"], 1, align="right")
|
||||
pix.draw_text(c, CV_W - 10, 40, "PAUSED", C["dim"], 1, align="right")
|
||||
|
||||
# ------------------------------------------------------------------ title
|
||||
def _title_update(self):
|
||||
@@ -837,12 +838,12 @@ class Game:
|
||||
|
||||
def _render_title(self, c):
|
||||
c.blit(self.bg, (0, 0))
|
||||
# ringed planet
|
||||
pygame.draw.circle(c, (150, 110, 220), (90, 70), 24)
|
||||
pygame.draw.circle(c, (190, 160, 245), (90, 70), 24, 2)
|
||||
pygame.draw.ellipse(c, (255, 200, 220), (58, 64, 64, 18), 2)
|
||||
pygame.draw.circle(c, (110, 90, 200), (300, 320), 10)
|
||||
pygame.draw.circle(c, (110, 90, 200), (300, 320), 10, 1)
|
||||
# ringed planet top-left, little moon bottom-right
|
||||
pygame.draw.circle(c, (150, 110, 220), (62, 80), 22)
|
||||
pygame.draw.circle(c, (190, 160, 245), (62, 80), 22, 2)
|
||||
pygame.draw.ellipse(c, (255, 200, 220), (34, 74, 56, 16), 2)
|
||||
pygame.draw.circle(c, (110, 90, 200), (310, 380), 12)
|
||||
pygame.draw.circle(c, (150, 120, 230), (310, 380), 12, 1)
|
||||
# twinkling stars
|
||||
for sx, sy, tw, ph in self.stars_bg:
|
||||
if (self.frame + ph) % 70 < 6:
|
||||
@@ -852,57 +853,55 @@ class Game:
|
||||
c.fill(p["color"], (int(p["x"]), int(p["y"]), 2, 2))
|
||||
|
||||
# logo
|
||||
words = [("COSMIC", 3, 36, C["pink"]), ("CANDY", 3, 62, C["gold"]),
|
||||
("PINBALL", 2, 108, C["mint"])]
|
||||
y = 40
|
||||
for txt, sc, _, col in words:
|
||||
y = 44
|
||||
for txt, sc, col in (("COSMIC", 3, C["pink"]), ("CANDY", 3, C["gold"]),
|
||||
("PINBALL", 2, C["mint"])):
|
||||
tw = pix.text_width(txt, sc)
|
||||
x = CV_W // 2 - tw // 2
|
||||
for i, ch in enumerate(txt):
|
||||
col2 = col
|
||||
pix.draw_text(c, x, y, ch, col2, sc, shadow=(20, 12, 50))
|
||||
pix.draw_text(c, x, y, ch, col, sc, shadow=(20, 12, 50))
|
||||
x += (5 + 1) * sc
|
||||
y += (5 + 2) * sc
|
||||
# little shooting star occasionally
|
||||
if self.frame % 90 < 24:
|
||||
t0 = self.frame % 90
|
||||
sx = 330 + t0 * 3
|
||||
sy = 40 + t0 * 1.5
|
||||
sx = 250 + t0 * 2
|
||||
sy = 130 + t0
|
||||
pygame.draw.line(c, (255, 255, 255), (int(sx), int(sy)), (int(sx - 8), int(sy - 4)), 1)
|
||||
|
||||
# cat
|
||||
draw_body(c, CV_W // 2 - 26, 180, 2)
|
||||
draw_cat(c, CV_W // 2 - 26, 168, 2, "happy", self.frame)
|
||||
draw_body(c, CV_W // 2 - 24, 208, 2)
|
||||
draw_cat(c, CV_W // 2 - 24, 196, 2, "happy", self.frame)
|
||||
# instructions panel
|
||||
pygame.draw.rect(c, (22, 15, 56), (70, 240, CV_W - 140, 74), border_radius=8)
|
||||
pygame.draw.rect(c, C["rail"], (70, 240, CV_W - 140, 74), 2, border_radius=8)
|
||||
pygame.draw.rect(c, (22, 15, 56), (30, 262, CV_W - 60, 84), border_radius=8)
|
||||
pygame.draw.rect(c, C["rail"], (30, 262, CV_W - 60, 84), 2, border_radius=8)
|
||||
lines = [
|
||||
("LEFT / RIGHT ARROWS = FLIPPERS", C["flipper"]),
|
||||
("HOLD SPACE OR UP = CHARGE CANNON", C["gold"]),
|
||||
("M MUTE R RESTART P PAUSE", C["dim"]),
|
||||
]
|
||||
for i, (txt, col) in enumerate(lines):
|
||||
pix.draw_text(c, CV_W // 2, 250 + i * 20, txt, col, 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 272 + i * 20, txt, col, 1, align="center")
|
||||
|
||||
if (self.frame // 30) % 2 == 0:
|
||||
pix.draw_text(c, CV_W // 2, 325, "PRESS ENTER TO START", C["white"], 2, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 378, "PRESS ENTER TO START", C["white"], 2, align="center")
|
||||
|
||||
# ------------------------------------------------------------------ game over
|
||||
def _render_over(self, c):
|
||||
self._render_play(c)
|
||||
self.overlay.fill((14, 9, 40, 215))
|
||||
c.blit(self.overlay, (0, 0))
|
||||
pix.draw_text(c, CV_W // 2, 60, "GAME OVER", C["pink"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 105, "SCORE", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 115, "{:,}".format(self.score), C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 160, "BEST", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 170, "{:,}".format(self.hi), C["gold"], 2, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 70, "GAME OVER", C["pink"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 128, "SCORE", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 140, "{:,}".format(self.score), C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 195, "BEST", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 207, "{:,}".format(self.hi), C["gold"], 2, align="center")
|
||||
if self.new_hi and (self.frame // 20) % 2 == 0:
|
||||
pix.draw_text(c, CV_W // 2, 205, "NEW HI-SCORE!", C["gold"], 1, align="center")
|
||||
draw_body(c, CV_W // 2 - 20, 240, 2)
|
||||
draw_cat(c, CV_W // 2 - 20, 226, 2, "sad", self.frame)
|
||||
pix.draw_text(c, CV_W // 2, 250, "NEW HI-SCORE!", C["gold"], 1, align="center")
|
||||
draw_body(c, CV_W // 2 - 20, 310, 2)
|
||||
draw_cat(c, CV_W // 2 - 20, 296, 2, "sad", self.frame)
|
||||
if (self.frame // 30) % 2 == 0:
|
||||
pix.draw_text(c, CV_W // 2, 320, "PRESS ENTER TO PLAY AGAIN", C["white"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 400, "PRESS ENTER TO PLAY AGAIN", C["white"], 1, align="center")
|
||||
|
||||
# ------------------------------------------------------------------ input
|
||||
def event(self, ev):
|
||||
|
||||
Reference in New Issue
Block a user