Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from mastermind_overlay.boss_mutators.catalog.color_ban import ColorBan
from mastermind_overlay.boss_mutators.catalog.white_peg_blind import WhitePegBlind
from mastermind_overlay.boss_mutators.mutator import BossMutator


BOSS_MUTATORS: list[type[BossMutator]] = [
ColorBan,
WhitePegBlind,
]

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import random
from typing import override

from mastermind_kernel.colors import PegColor

from mastermind_overlay.boss_mutators.mutator import BossMutator


class ColorBan(BossMutator):
def __init__(self) -> None:
self._banned_color: PegColor = random.choice(list(PegColor))

@property
def banned_color(self) -> PegColor:
return self._banned_color

@property
@override
def description(self) -> str:
return f"{self._banned_color.name.title()} is banned this round"


def allowed_colors(self) -> set[PegColor] | None:
return set(PegColor) - {self._banned_color}


Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import override

from mastermind_kernel.feedback import CodeFeedback

from mastermind_overlay.boss_mutators.mutator import BossMutator


class WhitePegBlind(BossMutator):
@property
@override
def description(self) -> str:
return "White pegs are hidden this round"


@override
def transform_feedback(self, feedback: CodeFeedback) -> CodeFeedback:
return feedback._replace(white_pegs=0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

from mastermind_kernel.colors import PegColor
from mastermind_kernel.feedback import CodeFeedback

if TYPE_CHECKING:
from mastermind_overlay.run.gamestate import GameState


class BossMutator(ABC):
@property
@abstractmethod
def description(self) -> str: ...


def transform_feedback(self, feedback: CodeFeedback) -> CodeFeedback:
return feedback


def allowed_colors(self) -> set[PegColor] | None:
return None


def on_round_start(self, state: GameState) -> None:
pass

Empty file.
19 changes: 19 additions & 0 deletions packages/mastermind-overlay/src/mastermind_overlay/events/bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from collections.abc import Callable
from typing import Any, TypeVar


E = TypeVar("E")

class EventBus:
def __init__(self) -> None:
self._handlers: dict[type, list[Callable[[Any], None]]] = {}


def subscribe(self, event_type: type[E], handler: Callable[[E], None]) -> None:
self._handlers.setdefault(event_type, []).append(handler)


def publish(self, event: object) -> None:
for handler in self._handlers.get(type(event), []):
handler(event)

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass

from mastermind_overlay.run.round_tier import RoundTier


@dataclass(frozen=True)
class RoundStarted:
tier: RoundTier
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from dataclasses import dataclass


@dataclass(frozen=True)
class ShopEntered:
pass
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@
from mastermind_kernel.feedback import CodeFeedback

from mastermind_overlay.relics.relic import Relic
from mastermind_overlay.scoring.points import Points, apply_multiplier, compute_points
from mastermind_overlay.scoring.pipeline import ScoringPipeline
from mastermind_overlay.scoring.points import apply_multiplier, compute_points
from mastermind_overlay.boss_mutators.mutator import BossMutator


class GameState:
def __init__(self, max_turns: int, target_score: int, relics: list[Relic]) -> None:
def __init__(self, max_turns: int, target_score: int, relics: list[Relic], mutator: BossMutator | None = None) -> None:
self._max_turns: int = max_turns
self._target_score: int = target_score
self._current_turn: int = 0
self._current_peg: int = 0
self._mutator: BossMutator | None = mutator
self._current_code: Code = Code.blank()
self._sanitize_current_code()
self._secret_code: Code = Code.random()
self._guessed_codes: list[Code] = []
self._guessed_feedback: list[CodeFeedback] = []
self._score: int = 0
self._relics: list[Relic] = relics
self._pipeline: ScoringPipeline = ScoringPipeline(relics)

@property
def current_peg(self) -> int:
Expand Down Expand Up @@ -62,15 +67,20 @@ def game_over(self) -> bool:
def target_score(self) -> int:
return self._target_score

@property
def mutator(self) -> BossMutator | None:
return self._mutator


def submit_guess(self) -> None:
self._guessed_codes.append(self._current_code)
feedback: CodeFeedback = self._secret_code.feedback(self._current_code)
self._guessed_feedback.append(feedback)
displayed_feedback = feedback
if self._mutator is not None:
displayed_feedback = self._mutator.transform_feedback(feedback)
self._guessed_feedback.append(displayed_feedback)

mult: int = 1
points: Points = compute_points(feedback)
for relic in self._relics:
points, mult = relic.on_guess(points, mult, feedback)
points, mult = self._pipeline.run(compute_points(feedback), 1, feedback)
self._score += apply_multiplier(points, mult)
self._current_turn += 1

Expand All @@ -81,6 +91,25 @@ def add_turn(self) -> None:

def cycle_color(self, direction: int) -> None:
self._current_code.cycle(self._current_peg, direction)
self._enforce_allowed_color(self._current_peg, direction)


def _enforce_allowed_color(self, peg: int, direction: int = 1) -> None:
if self._mutator is None:
return

allowed = self._mutator.allowed_colors()
if allowed is None:
return

while self._current_code[peg] not in allowed:
self._current_code.cycle(peg, direction)


def _sanitize_current_code(self) -> None:
for peg in range(len(self._current_code)):
self._enforce_allowed_color(peg)


def cycle_current_square(self, direction: int) -> None:
if direction > 0:
Expand All @@ -92,6 +121,7 @@ def cycle_current_square(self, direction: int) -> None:

def reset_current_guess(self) -> None:
self._current_code = Code.blank()
self._sanitize_current_code()
self._current_peg = 0

def reset(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import random

from mastermind_overlay.economy.currency import reward_for
from mastermind_overlay.events.bus import EventBus
from mastermind_overlay.events.catalog.round_started import RoundStarted
from mastermind_overlay.relics.relic import Relic
from mastermind_overlay.run.gamestate import GameState
from mastermind_overlay.run.round_tier import RoundTier
from mastermind_overlay.boss_mutators.catalog import BOSS_MUTATORS
from mastermind_overlay.boss_mutators.mutator import BossMutator


class RunState:
Expand All @@ -17,6 +23,7 @@ def __init__(self) -> None:
self._max_relics: int = 5
self._currency: int = 0
self._tier_idx: int = 0
self._events: EventBus = EventBus()


@property
Expand All @@ -35,6 +42,10 @@ def relics(self) -> list[Relic]:
def tier(self) -> RoundTier:
return self._TIER_ORDER[self._tier_idx]

@property
def events(self) -> EventBus:
return self._events

@property
def max_relics(self) -> int:
return self._max_relics
Expand All @@ -55,7 +66,13 @@ def reward_round(self, state: GameState) -> None:
def new_round(self) -> GameState:
stage_base = self._BASE_TARGET_SCORE * self._TARGET_SCORE_GROWTH ** (self._stage - 1)
target = round(stage_base * self.tier.multiplier)
return GameState(self._BASE_MAX_TURNS, target, self._relics)
mutator: BossMutator | None = self._pick_mutator() if self.tier.is_final else None
self._events.publish(RoundStarted(self.tier))
return GameState(self._BASE_MAX_TURNS, target, self._relics, mutator)
Comment on lines +69 to +71


def _pick_mutator(self) -> BossMutator:
return random.choice(BOSS_MUTATORS)()


def spend(self, amount: int) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Protocol, Sequence

from mastermind_kernel.feedback import CodeFeedback
from mastermind_overlay.scoring.points import Points


class ScoringStage(Protocol):
def on_guess(self, points: Points, mult: int, feedback: CodeFeedback) -> tuple[Points, int]: ...


class ScoringPipeline:
def __init__(self, stages: Sequence[ScoringStage]) -> None:
self._stages: Sequence[ScoringStage] = stages


def run(self, points: Points, mult: int, feedback: CodeFeedback) -> tuple[Points, int]:
for stage in self._stages:
points, mult = stage.on_guess(points, mult, feedback)
return points, mult
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,18 @@ def _draw_shop_cards(self, items: list[tuple[str, str, int]], selected: int, rel
self._stdscr.addstr(y_pos + card_height - 2, x + (card_width - len(price_str)) // 2, price_str, text_attr)

x += card_width + 1


def render_boss_announcement(self, title: str, description: str) -> None:
self._stdscr.clear()
self._stdscr.addstr(
curses.LINES // 2,
(curses.COLS - len(title)) // 2,
title,
curses.A_STANDOUT)
self._stdscr.addstr(
curses.LINES // 2 + 1,
(curses.COLS - len(description)) // 2,
description,
curses.A_STANDOUT)
self._stdscr.refresh()
Comment on lines +247 to +259
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ def render_gameplay(self, state: GameState, stage: int, tier_label: str, relics:
def render_win(self, correct_guess_str: str) -> None: ...
def render_loose(self, correct_guess_str: str) -> None: ...
def render_shop(self, currency: int, offer: list[Relic], extra_guess_price: int, selected: int, owned_relics: list[Relic], max_relics: int) -> None: ...
def render_boss_announcement(self, title: str, description: str) -> None: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import override

from mastermind_overlay.boss_mutators.mutator import BossMutator
from mastermind_shell.engine.input_event import InputEvent
from mastermind_shell.engine.renderer import Renderer
from mastermind_shell.engine.screen import Screen
from mastermind_shell.engine.screen_queue import ScreenQueue
from mastermind_shell.engine.transition import ScreenTransition


class BossAnnouncementScreen(Screen):
def __init__(self, queue: ScreenQueue, renderer: Renderer, mutator: BossMutator) -> None:
super().__init__(queue, True)
self._renderer: Renderer = renderer
self._mutator: BossMutator = mutator


@override
def handle_input(self, event: InputEvent) -> None:
self._queue.push(ScreenTransition.pop())


@override
def update(self) -> None:
pass


@override
def render(self) -> None:
self._renderer.render_boss_announcement("BOSS ROUND", self._mutator.description)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import random
from typing import override

from mastermind_overlay.events.catalog.shop_entered import ShopEntered
from mastermind_overlay.relics.catalog import RELICS
from mastermind_overlay.relics.relic import Relic
from mastermind_overlay.run.gamestate import GameState
Expand All @@ -11,6 +12,7 @@
from mastermind_shell.engine.screen import Screen
from mastermind_shell.engine.screen_queue import ScreenQueue
from mastermind_shell.engine.transition import ScreenTransition
from mastermind_shell.screens.boss_announcement import BossAnnouncementScreen

_OFFER_SIZE: int = 3
_EXTRA_GUESS_PRICE: int = 8
Expand All @@ -30,6 +32,7 @@ def __init__(self, queue: ScreenQueue, renderer: Renderer, run_state: RunState,
self._next_state: GameState = next_state
self._offer: list[Relic] = generate_offer(run_state.relics)
self._cursor: int = 0
run_state.events.publish(ShopEntered())


@override
Expand All @@ -51,6 +54,10 @@ def handle_input(self, event: InputEvent) -> None:
self._buy_extra_guess()
else:
self._queue.push(ScreenTransition.pop())
if self._next_state.mutator is not None:
self._queue.push(ScreenTransition.push(
BossAnnouncementScreen(self._queue, self._renderer, self._next_state.mutator)
))
if event == InputEvent.QUIT:
self._queue.push(ScreenTransition.pop())

Expand Down