Before running the GUI, install pygame once:
Then run:
pip install pygame
or
pip3 install pygameThen run:
python3 hog_gui.py
"""
hog_gui.py — The Game of Hog (pygame GUI)
Before running, install pygame once:
pip install pygame
Run: python3 hog_gui.py
vs CPU: python3 hog_gui.py -f
Keys: 0-9 roll count | ` for 10 | r restart
"""
import sys, threading, queue, random, time, math, argparse
try:
import pygame
except ImportError:
print()
print(" pygame is required: pip install pygame")
print(" or: pip3 install pygame")
print()
sys.exit(1)
import hog, dice
from ucb import main
# ═══════════════════════════════════════════════════════
# Window
# ═══════════════════════════════════════════════════════
W, H = 860, 580
FPS = 60
DELAY = 1500 # computer thinking ms
# ═══════════════════════════════════════════════════════
# Layout — three column: [player 0] [dice arena] [player 1]
# ═══════════════════════════════════════════════════════
HDR_H = 50
PANEL_W = 170
CTR_X = PANEL_W
CTR_W = W - 2 * PANEL_W # 520
# Dice (2 rows × 5, each 80px)
DIE_SZ = 80
DIE_GAP = 9
_DTW = 5 * DIE_SZ + 4 * DIE_GAP # 436
DICE_X0 = CTR_X + (CTR_W - _DTW) // 2
DICE_Y0 = HDR_H + 38
DICE_Y1 = DICE_Y0 + DIE_SZ + DIE_GAP
# Roll tiles — two rows (0–5 on top, 6–10 below)
TW, TH, TGAP = 62, 42, 7
_TR0 = 6 * TW + 5 * TGAP # 397
_TR1 = 5 * TW + 4 * TGAP # 338
TILE_Y0 = H - 132
TILE_Y1 = TILE_Y0 + TH + 8
TILE_X0 = CTR_X + (CTR_W - _TR0) // 2
TILE_X1 = CTR_X + (CTR_W - _TR1) // 2
# ═══════════════════════════════════════════════════════
# CS7 palette — warm parchment + mint + terracotta
# ═══════════════════════════════════════════════════════
C_BG = (236, 230, 216) # parchment
C_CTR = (250, 246, 238) # dice arena (lighter)
C_BDR = (204, 196, 180) # borders
C_P0_BG = (207, 241, 231) # mint-washed player 0 panel
C_P1_BG = (252, 226, 206) # peach player 1 panel
C_P0_DIM = (228, 238, 234) # dimmed panel (not your turn)
C_P1_DIM = (248, 236, 230)
C_MINT = ( 77, 187, 160)
C_MINTD = ( 46, 138, 115)
C_TERRA = (201, 104, 58)
C_TERRD = (154, 78, 36)
C_INK = ( 30, 24, 16)
C_INK2 = ( 70, 62, 52)
C_DIM = (128, 118, 104)
C_BDIM = (178, 170, 158)
C_AMBER = (196, 134, 14)
C_RED = (188, 44, 44)
C_GREEN = ( 48, 148, 80)
C_GOLD = (172, 128, 8)
C_WHITE = (255, 255, 255)
P_COL = [C_MINT, C_TERRA]
P_DARK = [C_MINTD, C_TERRD]
P_ON = [C_P0_BG, C_P1_BG]
P_OFF = [C_P0_DIM, C_P1_DIM]
# Die dots (0–100 coords, scaled to die size)
_DOTS = {
1: [(50, 50)],
2: [(28, 28), (72, 72)],
3: [(28, 28), (50, 50), (72, 72)],
4: [(28, 28), (72, 28), (28, 72), (72, 72)],
5: [(28, 28), (72, 28), (50, 50), (28, 72), (72, 72)],
6: [(28, 28), (72, 28), (28, 50), (72, 50), (28, 72), (72, 72)],
}
# ═══════════════════════════════════════════════════════
# Drawing helpers
# ═══════════════════════════════════════════════════════
def rrect(surf, col, rect, r, border=None, bw=1):
pygame.draw.rect(surf, col, rect, border_radius=r)
if border:
pygame.draw.rect(surf, border, rect, bw, border_radius=r)
def draw_die(surf, x, y, value, pig=False, size=DIE_SZ):
bg = (255, 234, 234) if pig else C_WHITE
bdr = (212, 186, 186) if pig else C_BDR
dot = C_RED if (pig and value == 1) else C_INK
# Shadow
pygame.draw.rect(surf, C_BDR, (x + 3, y + 4, size, size), border_radius=13)
# Face
pygame.draw.rect(surf, bg, (x, y, size, size), border_radius=13)
pygame.draw.rect(surf, bdr, (x, y, size, size), 2, border_radius=13)
# Dots
dr = max(4, size * 7 // 100)
for px, py in _DOTS.get(value, []):
cx = x + px * size // 100
cy = y + py * size // 100
pygame.draw.circle(surf, dot, (cx, cy), dr)
def pbar(surf, x, y, w, h, pct, col, bg=C_BDR, r=4):
pygame.draw.rect(surf, bg, (x, y, w, h), border_radius=r)
fw = max(0, int(min(pct, 1.0) * w))
if fw > r:
pygame.draw.rect(surf, col, (x, y, fw, h), border_radius=r)
def blit_c(surf, t, cx, y):
surf.blit(t, t.get_rect(centerx=cx, y=y))
# ═══════════════════════════════════════════════════════
# Die sprite (spring-physics scale-in + face cycling)
# ═══════════════════════════════════════════════════════
class DieSprite:
NFRAMES = 8
def __init__(self, slot):
self.slot = slot
self.visible = False
self.value = 6
self.disp = 6
self.pig = False
self.rolling = False
self.frame = 0
self.timer = 0.0
self.sc = 0.0 # spring scale 0→1
self.vel = 0.0
def start(self, final):
self.value = final
self.pig = (final == 1)
self.disp = random.randint(1, 6)
self.rolling = True
self.frame = 0
self.timer = 0.0
self.sc = 0.0
self.vel = 0.065
self.visible = True
def update(self, dt):
if self.sc < 1.0 or abs(self.vel) > 0.002:
self.vel += (1.0 - self.sc) * 0.014
self.vel *= 0.81
self.sc += self.vel * dt / 16.0
if self.sc > 1.06 and self.vel < 0.01:
self.sc, self.vel = 1.0, 0.0
if not self.rolling:
return
intv = 40 + self.frame * 18
self.timer += dt
if self.timer >= intv:
self.timer = 0.0
self.frame += 1
if self.frame >= self.NFRAMES:
self.rolling = False
self.disp = self.value
else:
self.disp = random.randint(1, 6)
def draw(self, surf, x, y):
if not self.visible:
return
s = max(8, int(DIE_SZ * min(self.sc, 1.12)))
off = (DIE_SZ - s) // 2
pig = self.pig and not self.rolling
draw_die(surf, x + off, y + off, self.disp, pig=pig, size=s)
# ═══════════════════════════════════════════════════════
# Particle
# ═══════════════════════════════════════════════════════
class Particle:
def __init__(self, x, y, col):
self.x = float(x)
self.y = float(y)
self.vx = random.uniform(-5.5, 5.5)
self.vy = random.uniform(-8.0, -2.0)
self.life = random.randint(45, 100)
self.max = self.life
self.r = random.randint(3, 8)
self.col = col
def update(self):
self.x += self.vx
self.y += self.vy
self.vy += 0.24
self.life -= 1
def draw(self, surf):
frac = max(0.0, self.life / self.max)
c = tuple(int(bg + (v - bg) * frac) for v, bg in zip(self.col, C_BG))
pygame.draw.circle(surf, c, (int(self.x), int(self.y)), max(1, self.r))
@property
def alive(self): return self.life > 0
# ═══════════════════════════════════════════════════════
# Kill signal
# ═══════════════════════════════════════════════════════
class _Kill(BaseException):
pass
# ═══════════════════════════════════════════════════════
# Game
# ═══════════════════════════════════════════════════════
class HogGame:
def __init__(self, computer=False):
pygame.init()
self.screen = pygame.display.set_mode((W, H), pygame.RESIZABLE)
pygame.display.set_caption('The Game of Hog — CS7')
self.clock = pygame.time.Clock()
self.computer = computer
self._fonts()
# State
self.who = 0
self.scores = [0, 0]
self.state = 'waiting' # waiting | rolling | gameover
self.prompt = ''
self.prompt_col = C_INK
self.sub = ''
self.sub_col = C_DIM
self.winner = None
self.particles = []
self.sprites = [DieSprite(i) for i in range(10)]
self.dice_count = 0
# Running turn score
self.turn_rolls = []
self.turn_pig = False
self.turn_total = 0
# Flash effect
self.flash_t = 0.0
self.flash_col = C_RED
# Win overlay alpha
self.win_alpha = 0.0
# Queues
self.roll_q = queue.Queue()
self.evt_q = queue.Queue()
self._kill = threading.Event()
self._gt = None
self._build_tiles()
self._rst = self._btn(W - 126, 12, 112, 30, '↺ New Game', self._restart)
self._new_game()
# ── Fonts ──────────────────────────────────────────
def _fonts(self):
self.fT = pygame.font.SysFont('Georgia', 19, bold=True) # title
self.fPN = pygame.font.SysFont('Helvetica', 11, bold=True) # player name
self.fSC = pygame.font.SysFont('Helvetica', 56, bold=True) # score
self.fSC2= pygame.font.SysFont('Helvetica', 22, bold=True) # small score
self.fPR = pygame.font.SysFont('Helvetica', 13, bold=True) # prompt
self.fST = pygame.font.SysFont('Helvetica', 12) # status
self.fTL = pygame.font.SysFont('Helvetica', 15, bold=True) # tile number
self.fHT = pygame.font.SysFont('Helvetica', 11) # hint
self.fBD = pygame.font.SysFont('Helvetica', 11, bold=True) # badge
self.fWN = pygame.font.SysFont('Georgia', 36, bold=True) # win overlay
# ── Button helpers ─────────────────────────────────
def _btn(self, x, y, w, h, label, cb):
return {'rect': pygame.Rect(x, y, w, h), 'label': label, 'cb': cb}
def _build_tiles(self):
self.tiles = []
for n in range(11):
row = 0 if n <= 5 else 1
col = n if n <= 5 else n - 6
tx = (TILE_X0 if row == 0 else TILE_X1) + col * (TW + TGAP)
ty = TILE_Y0 if row == 0 else TILE_Y1
self.tiles.append(self._btn(tx, ty, TW, TH, str(n),
lambda v=n: self._pick(v)))
# ── Game thread ────────────────────────────────────
def _new_game(self):
self._kill.clear()
self.who, self.scores = 0, [0, 0]
self.state = 'waiting'
self.winner = None
self.dice_count = 0
self.particles = []
self.prompt = self.sub = ''
self.turn_rolls = []
self.turn_pig = False
self.turn_total = 0
self.flash_t = 0.0
self.win_alpha = 0.0
for s in self.sprites:
s.visible = False
for q in (self.roll_q, self.evt_q):
while not q.empty():
try: q.get_nowait()
except queue.Empty: break
self._gt = threading.Thread(target=self._game_thread, daemon=True)
self._gt.start()
def _game_thread(self):
hog.six_sided = self._mkdice(6)
hog.four_sided = self._mkdice(4)
try:
s0, s1 = hog.play(
lambda s, o: self._strat(0, s, o),
lambda s, o: self._strat(1, s, o),
)
self.evt_q.put(('gameover', s0, s1))
except _Kill:
self.evt_q.put(('killed',))
except Exception as e:
self.evt_q.put(('error', str(e)))
def _mkdice(self, sides):
fair = dice.make_fair_dice(sides)
def gui_dice():
if self._kill.is_set(): raise _Kill
val = fair()
self.evt_q.put(('die', val, sides))
t0 = time.monotonic()
while time.monotonic() - t0 < 0.43:
if self._kill.is_set(): raise _Kill
time.sleep(0.04)
return val
return gui_dice
def _strat(self, player, score, opp):
if self._kill.is_set(): raise _Kill
self.evt_q.put(('turn', player, score, opp))
if self.computer and player == 1:
t0 = time.monotonic()
while time.monotonic() - t0 < DELAY / 1000.0:
if self._kill.is_set(): raise _Kill
time.sleep(0.05)
try: n = hog.final_strategy(score, opp)
except: n = 4
else:
n = self.roll_q.get()
if n < 0 or self._kill.is_set(): raise _Kill
self.evt_q.put(('rolling', n))
return n
# ── Events ─────────────────────────────────────────
def _drain(self):
try:
while True:
ev = self.evt_q.get_nowait()
tag = ev[0]
if tag == 'killed':
self._new_game()
elif tag == 'turn':
_, p, sc, opp = ev
self.who = p
self.scores[p] = sc
self.scores[1 - p] = opp
self.state = 'waiting'
self.dice_count = 0
self.turn_rolls = []
self.turn_pig = False
self.turn_total = 0
for s in self.sprites: s.visible = False
try: wild = hog.select_dice(sc, opp) == hog.four_sided
except: wild = False
if wild:
self.prompt = 'HOG WILD! Rolling 4-sided dice — pick count:'
self.prompt_col = C_AMBER
else:
self.prompt = 'How many dice?'
self.prompt_col = P_COL[p]
self.sub = ''
elif tag == 'rolling':
_, n = ev
self.state = 'rolling'
self.prompt = f'Rolling {n} {"die" if n == 1 else "dice"}...'
elif tag == 'die':
_, val, _ = ev
idx = self.dice_count
if idx < 10:
self.sprites[idx].start(val)
self.dice_count += 1
self.turn_rolls.append(val)
if val == 1:
self.turn_pig = True
self.flash_t = 420.0
self.flash_col = C_RED
x, y = self._dxy(self.dice_count - 1)
for _ in range(28):
self.particles.append(
Particle(x + DIE_SZ // 2, y + DIE_SZ // 2, C_TERRA))
else:
self.turn_total += val
elif tag == 'gameover':
_, s0, s1 = ev
self.scores = [s0, s1]
self.winner = 0 if s0 > s1 else 1
self.state = 'gameover'
self.prompt = ''
col = P_COL[self.winner]
self.flash_t = 0.0
self.win_alpha = 0.0
for _ in range(100):
c = random.choice([col, P_DARK[self.winner],
C_AMBER, (255, 210, 100), C_WHITE])
self.particles.append(
Particle(random.randint(60, W - 60), H // 3, c))
elif tag == 'error':
self.sub, self.sub_col = f'Error: {ev[1]}', C_RED
except queue.Empty:
pass
# ── Positions ──────────────────────────────────────
def _dxy(self, idx):
row = idx // 5
col = idx % 5
return DICE_X0 + col * (DIE_SZ + DIE_GAP), \
(DICE_Y0 if row == 0 else DICE_Y1)
# ══════════════════════════════════════════════════════
# Draw
# ══════════════════════════════════════════════════════
def _draw_player(self, p, active, t):
x = 0 if p == 0 else W - PANEL_W
bg = P_ON[p] if active else P_OFF[p]
sc = self.scores[p]
col = P_COL[p]
# Panel background
pygame.draw.rect(self.screen, bg, (x, HDR_H, PANEL_W, H - HDR_H))
# Inner colored stripe (on the edge nearest the center)
edge_x = PANEL_W - 4 if p == 0 else W - PANEL_W
stripe_col = col if active else C_BDIM
pygame.draw.rect(self.screen, stripe_col, (edge_x, HDR_H, 4, H - HDR_H))
# Player label
px = x + PANEL_W // 2
nc = col if active else C_BDIM
lbl = self.fPN.render(f'PLAYER {p}', True, nc)
self.screen.blit(lbl, lbl.get_rect(centerx=px, y=HDR_H + 16))
# Score
sc_col = C_INK if active else C_BDIM
t_sc = self.fSC.render(str(sc), True, sc_col)
self.screen.blit(t_sc, t_sc.get_rect(centerx=px, y=HDR_H + 34))
# "to 100" label
rem = self.fHT.render(f'{max(0, 100-sc)} to go', True, C_DIM if active else C_BDIM)
self.screen.blit(rem, rem.get_rect(centerx=px, y=HDR_H + 98))
# Progress bar
bar_w = PANEL_W - 28
bar_x = x + 14
bar_y = HDR_H + 115
pbar(self.screen, bar_x, bar_y, bar_w, 9, sc / 100.0, col,
bg=C_BDR if active else C_BDIM)
# Milestones (25, 50, 75)
for m in (25, 50, 75):
mx = bar_x + int(m / 100.0 * bar_w)
col_tick = C_DIM if active else C_BDIM
pygame.draw.line(self.screen, col_tick, (mx, bar_y - 2), (mx, bar_y + 11), 1)
# YOUR TURN badge (pulsing)
if active and self.state in ('waiting', 'rolling'):
pulse = 0.75 + 0.25 * math.sin(t * 3.0)
bw = int(PANEL_W - 24)
bh = 26
by = HDR_H + 140
bx = x + 12
alpha_bg = int(220 * pulse)
badge_surf = pygame.Surface((bw, bh), pygame.SRCALPHA)
r_col = (*col, alpha_bg)
pygame.draw.rect(badge_surf, r_col, (0, 0, bw, bh), border_radius=7)
self.screen.blit(badge_surf, (bx, by))
arrow = '▶' if p == 0 else '◀'
label = f'{arrow} YOUR TURN'
bt = self.fBD.render(label, True, C_WHITE)
self.screen.blit(bt, bt.get_rect(centerx=x + PANEL_W // 2, y=by + 7))
def _draw_center(self, t):
# Center panel background
pygame.draw.rect(self.screen, C_CTR,
(CTR_X, HDR_H, CTR_W, H - HDR_H))
# Subtle inner shadow top
for i in range(5):
a = 30 - i * 6
if a > 0:
s = pygame.Surface((CTR_W, 1), pygame.SRCALPHA)
s.fill((0, 0, 0, a))
self.screen.blit(s, (CTR_X, HDR_H + i))
# "DICE" label
dl = self.fHT.render('D I C E', True, C_DIM)
blit_c(self.screen, dl, CTR_X + CTR_W // 2, HDR_H + 16)
# Separator line under dice label
pygame.draw.line(self.screen, C_BDR,
(CTR_X + 20, HDR_H + 30), (CTR_X + CTR_W - 20, HDR_H + 30), 1)
# Dice sprites
for i, sp in enumerate(self.sprites):
sp.update(self.clock.get_time())
if sp.visible:
x, y = self._dxy(i)
sp.draw(self.screen, x, y)
# Turn total / pig-out display
ty = DICE_Y1 + DIE_SZ + 14
if self.turn_rolls:
if self.turn_pig:
msg = self.fPR.render('PIG OUT! Turn score: 1', True, C_RED)
blit_c(self.screen, msg, CTR_X + CTR_W // 2, ty)
else:
total = self.turn_total
rolls = '+'.join(str(r) for r in self.turn_rolls)
label = f'{rolls} = {total}' if len(self.turn_rolls) > 1 else f'= {total}'
msg = self.fPR.render(f'Turn so far: {label}', True, C_GREEN)
blit_c(self.screen, msg, CTR_X + CTR_W // 2, ty)
# Prompt (e.g. "Player 0 — how many dice?")
if self.prompt:
# Colour band behind prompt
band_y = ty + 22
band = pygame.Surface((CTR_W, 24), pygame.SRCALPHA)
c = (*(self.prompt_col), 22)
band.fill(c)
self.screen.blit(band, (CTR_X, band_y))
pt = self.fPR.render(self.prompt, True, self.prompt_col)
blit_c(self.screen, pt, CTR_X + CTR_W // 2, band_y + 4)
# Separator above tiles
pygame.draw.line(self.screen, C_BDR,
(CTR_X + 12, TILE_Y0 - 12), (CTR_X + CTR_W - 12, TILE_Y0 - 12), 1)
def _draw_tiles(self):
en = (self.state == 'waiting')
col = P_COL[self.who]
mpos = pygame.mouse.get_pos()
for btn in self.tiles:
r = btn['rect']
hov = r.collidepoint(mpos) and en
if hov:
bg = col
tc = C_WHITE
bdr = col
elif en:
bg = C_WHITE
tc = C_INK
bdr = C_BDR
else:
bg = C_CTR
tc = C_BDIM
bdr = C_BDR
rrect(self.screen, bg, r, 9, bdr, 2)
t = self.fTL.render(btn['label'], True, tc)
self.screen.blit(t, t.get_rect(center=r.center))
def _draw_header(self):
# Cream header bar
pygame.draw.rect(self.screen, C_BG, (0, 0, W, HDR_H))
pygame.draw.line(self.screen, C_BDR, (0, HDR_H), (W, HDR_H), 1)
t = self.fT.render('The Game of Hog', True, C_INK)
cs = self.fHT.render('CS7', True, C_DIM)
self.screen.blit(t, (20, 14))
self.screen.blit(cs, (22 + t.get_width() + 8, 18))
# Restart button
r = self._rst['rect']
mpos = pygame.mouse.get_pos()
hov = r.collidepoint(mpos)
rrect(self.screen, C_TERRA if hov else C_WHITE,
r, 8, C_TERRA, 2)
tc = C_WHITE if hov else C_TERRA
bt = self.fBD.render(self._rst['label'], True, tc)
self.screen.blit(bt, bt.get_rect(center=r.center))
def _draw_win_overlay(self, dt):
if self.winner is None:
return
# Fade in
self.win_alpha = min(self.win_alpha + dt * 0.18, 210)
overlay = pygame.Surface((W, H), pygame.SRCALPHA)
bg_col = (*P_ON[self.winner], int(self.win_alpha))
overlay.fill(bg_col)
self.screen.blit(overlay, (0, 0))
t0 = self.fWN.render(f'Player {self.winner} wins!', True, P_DARK[self.winner])
blit_c(self.screen, t0, W // 2, H // 2 - 42)
s0, s1 = self.scores
t1 = self.fPR.render(f'Final score — Player 0: {s0} Player 1: {s1}',
True, C_INK2)
blit_c(self.screen, t1, W // 2, H // 2 + 4)
t2 = self.fST.render('Press r or click New Game to play again', True, C_DIM)
blit_c(self.screen, t2, W // 2, H // 2 + 36)
def _draw_flash(self, dt):
if self.flash_t <= 0:
return
frac = min(1.0, self.flash_t / 350.0)
alpha = int(90 * frac)
fs = pygame.Surface((W, H), pygame.SRCALPHA)
fs.fill((*self.flash_col, alpha))
self.screen.blit(fs, (0, 0))
self.flash_t = max(0.0, self.flash_t - dt)
def draw(self, dt):
t = time.monotonic()
self.screen.fill(C_BG)
self._draw_player(0, self.who == 0, t)
self._draw_player(1, self.who == 1, t)
self._draw_center(t)
self._draw_tiles()
self._draw_header()
# Particles (on top of everything except overlays)
for p in self.particles:
p.update(); p.draw(self.screen)
self.particles = [p for p in self.particles if p.alive]
self._draw_flash(dt)
if self.state == 'gameover':
self._draw_win_overlay(dt)
# Bottom hint
h = self.fHT.render('keys: 0-9 pick count · ` for 10 · r restart',
True, C_DIM)
blit_c(self.screen, h, W // 2, H - 16)
pygame.display.flip()
# ── Input ──────────────────────────────────────────
def _pick(self, n):
if self.state == 'waiting':
self.roll_q.put(n)
def _restart(self):
if self._gt and self._gt.is_alive():
self._kill.set()
self.roll_q.put(-1)
else:
self._new_game()
def _handle(self, ev):
if ev.type == pygame.QUIT:
return False
if ev.type == pygame.KEYDOWN:
k = ev.key
if k == pygame.K_r:
self._restart()
elif self.state == 'waiting':
if pygame.K_0 <= k <= pygame.K_9:
self._pick(k - pygame.K_0)
elif k == pygame.K_BACKQUOTE:
self._pick(10)
if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
if self._rst['rect'].collidepoint(ev.pos):
self._restart()
elif self.state == 'waiting':
for btn in self.tiles:
if btn['rect'].collidepoint(ev.pos):
btn['cb']()
return True
# ── Main loop ──────────────────────────────────────
def run(self):
while True:
dt = self.clock.tick(FPS)
for ev in pygame.event.get():
if not self._handle(ev):
self._kill.set()
pygame.quit()
return
self._drain()
self.draw(dt)
# ═══════════════════════════════════════════════════════
# Entry
# ═══════════════════════════════════════════════════════
def run_GUI(computer=False):
HogGame(computer=computer).run()
@main
def run(*args):
parser = argparse.ArgumentParser(description='Hog GUI — CS7')
parser.add_argument('-f', '--final', action='store_true',
help='Play against your final_strategy.')
parser.add_argument('-d', '--delay', type=int, default=2,
help='Computer thinking delay (seconds).')
args = parser.parse_args()
global DELAY
DELAY = args.delay * 1000
run_GUI(computer=args.final)