Skip to content

About

Mastermind and Super-Superhirn built as a full OOA to OOD to OOP case study: layered ports-and-adapters architecture, Strategy and Factory patterns, human vs algorithm vs algorithm modes, pluggable local or remote evaluation. Python, PySide6, UML 2.5.

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

Pixel-art arcade cabinet

SuperHirn

Mastermind, built as a full OOA to OOD to OOP case study

A desktop implementation of Superhirn and Super-Superhirn where the game is the artefact and the engineering process is the point: UML 2.5 analysis, a ports-and-adapters domain, and quality assurance carried through every phase.

Python UI License

Engineering documentation (PDF) · Architecture · Quick start · Modes


Why this repository exists

Mastermind is small enough to hold in your head and rich enough to have a real search problem. That made it a good vehicle for the actual question this project was built to answer:

What does it take to keep analysis, design and code consistent when the requirements keep changing?

Two significant requirements landed mid-project. A spectator mode, in which two algorithms race on the same secret. And a remote evaluation service, in which an external server grades the guesses. Neither was in the original analysis.

The architecture was designed so that new modes, new solvers and new evaluation sources arrive through composition rather than through branching. The result is measurable in the code: adding the spectator mode required no conditional logic inside the domain layer, and Test/test_architecture.py enforces the dependency direction on every CI run.

The complete engineering documentation, covering the analysis process, the Cockburn use case abstraction, fully specified use cases with acceptance criteria, the step-by-step design derivation and the QA strategy, is in mastermind_documentation.pdf. All UML diagrams are generated from source into figures/.


Play modes

Mode You are The app runs Remote evaluation
Guesser the codebreaker generates the secret, scores each guess supported
Code Maker the codemaker and the evaluator runs the chosen solver against your secret local only
Spectator an observer two solvers on one shared secret, side by side local only

In Code Maker mode you grade the computer's guesses yourself, and the system rejects feedback that is inconsistent with the code you set. In Spectator mode both boards evaluate against the same secret with a configurable per-round delay, which turns the mode into a direct visual benchmark of the two solvers.

Variant Code length Colours
Classic (Superhirn) 4 6
Advanced (Super-Superhirn) 5 8

Solvers

  • Consistency keeps the set of codes still compatible with every response so far and guesses from it. Deterministic candidate elimination.
  • Knuth is a minimax-inspired strategy with a fixed opening guess and a bounded scoring pass, chosen so the UI stays responsive.

Architecture

Five modules, one rule: all dependencies point inward, towards the domain.

flowchart LR
    UI["UI<br/>PySide6"] -->|IBoundary| APP["ApplicationControl"]
    APP -->|IGame| CORE["CoreDomainModel"]
    IMPL["Implementation<br/>solvers and providers"] -.->|implements ports| CORE
    NET["Networking<br/>HTTP client"] -.->|NetworkService| IMPL
    MAIN["main.py<br/>composition root"] --> APP
    MAIN --> IMPL
    MAIN --> NET
Loading
Module Responsibility Seam
UI/ Rendering and input. State-driven, refreshed on state change. IBoundary
ApplicationControl/ Use case orchestration: start, step, delay, mode flow. IGame
CoreDomainModel/ Rules, game state, round history, win and loss conditions. owns its ports
Implementation/ Concrete strategies: solvers, UI providers, system providers. domain ports
Networking/ HTTP adapter for the remote evaluator. NetworkService
Component diagram: five layers connected through explicit interfaces
Components and the interfaces between them. Every arrow crosses a named port. Full resolution: component-diagram.pdf

The three ports

Every mode is a different wiring of the same three interfaces:

IGuessProvider        # where does the next guess come from?
ISecretCodeProvider   # where does the secret code come from?
IEvaluationProvider   # who grades the guess?

ProviderFactory reads a GameConfig and returns a fully wired game:

Mode Secret code Guess Evaluation
Guesser (local) SystemSecretCodeProvider UIGuessProvider SystemEvaluationProvider
Guesser (online) server-owned UIGuessProvider RemoteEvaluationProvider
Code Maker UISecretCodeProvider AIGuessProvider(Knuth | Consistency) UIEvaluationProvider
Spectator one shared secret two solver instances SystemEvaluationProvider

This is why the UI contains no if mode == ... branches and the domain has no knowledge of where its inputs come from.

Patterns, and what each one bought

Pattern Where What it solved
Strategy the three provider ports Swapping input sources and solvers per mode without touching the domain
Factory Implementation/ProviderFactory.py One composition point instead of wiring logic scattered across UI and domain
Observer IBoundary callbacks UI updates on game events without the domain depending on the UI
Composition root main.py All concrete construction in one place

Details and the SOLID rationale are in docs/architecture.md.

The object model

The domain sits in the middle and defines the three ports. Everything below it is an adapter plugged into one of them, everything above talks to it through a single boundary interface.

Class diagram of the object model, organised by layer
Full resolution: class-diagram.pdf

State-driven UI

The UI does not decide state. It is refreshed with new data when the state changes, and mutually exclusive input states make invalid interactions structurally impossible: while a guess is expected, only the guess input is live, so feedback cannot be submitted before a guess exists.


How the design was derived

The design did not start from layers. It started from the observation that every mode is built out of the same three recurring subfunctions: obtain a secret code, obtain a guess, evaluate a guess. Those three became the ports.

Use cases were classified by Cockburn level. The subfunction (fish) level is where the recurrence becomes visible: the same handful of operations feeds all three user-facing modes, and those operations are exactly what became the provider ports.

Use case abstraction according to Cockburn
Full resolution: cockburn-abstraction.pdf

The three roles are modelled as «extend» rather than «include», because the player selects exactly one role per game, which makes the relationship conditional rather than mandatory.

UML use case diagram
Full resolution: use-case-diagram.pdf
Activity diagram, guesser mode (click to expand)

The full flow from configuration to game end, with the handover between player and system made explicit through swimlanes.

Activity diagram for guesser mode
Full resolution: activity-codebreaker.pdf

Quick start

Requires Python 3.10 or newer.

git clone https://github.com/theMindDeveloper/MasterMind.git
cd MasterMind

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\Activate.ps1

python -m pip install -r requirements.txt
python main.py

For development, including tests, linting and the mock server:

python -m pip install -r requirements-dev.txt

Windows build

The bundled artwork has to travel with the executable:

pyinstaller --noconfirm --windowed --name SuperHirn --add-data "assets;assets" main.py

Output lands in dist/SuperHirn/.


Online guesser mode

A small JSON protocol in which the server owns the secret. The client never creates or holds a local copy of it, which is enforced through IEvaluationProvider.uses_remote_secret().

A development server is bundled so the whole online path can be exercised locally:

python superhirn-server/server_mock.py

In the app choose Guesser → Online, then enter 127.0.0.1 and port 8080.

The bundled server is for local development only. It has no authentication, no encryption and no rate limiting. Do not expose it to an untrusted network.


Testing

ruff check .
python -m pytest
python -m pytest --cov --cov-report=term-missing

GitHub Actions runs the same lint and test steps on every push and pull request, with QT_QPA_PLATFORM=offscreen so the UI tests run headless.

Test file Covers
test_core_domain_model.py value objects, evaluation rules, state transitions
test_game.py the domain aggregate across all modes
test_algorithms.py Knuth and Consistency, including edge cases
test_provider_factory.py correct wiring per mode
test_providers.py each adapter against its port contract
test_network_service.py the HTTP protocol against a mock server
test_game_controller.py, test_game_screen.py, test_settings_view.py control and UI seams
test_end_to_end_modes.py complete games in every mode and variant
test_architecture.py dependency direction: the core imports no adapter

Repository map

main.py                 composition root
app/                    window shell and stylesheet
UI/                     PySide6 views, components, threads, utils
ApplicationControl/     IBoundary and GameController
CoreDomainModel/        rules, state, value objects, provider ports
Implementation/         solvers and local/UI/remote adapters
Networking/             HTTP protocol client
superhirn-server/       development-only remote evaluator
Test/                   unit, integration, UI and end-to-end tests
docs/                   architecture guide
figures/                UML diagrams (SVG and PDF)
assets/                 runtime artwork and licensing provenance

Known limitations

Written down deliberately. This was a course project under a fixed deadline, not a finished product.

  • IEvaluationProvider carries more than evaluation. Next to evaluate it exposes set_feedback, get_feedback_if_available and uses_remote_secret, so a domain port knows that "remote" exists and also carries a UI concern. Splitting it into an evaluation port and a human-input port is the first refactoring to do.
  • IGuessProvider.set_guess is optional. It raises NotImplementedError in the base and only UIGuessProvider implements it, so not every adapter is substitutable through the full interface.
  • AIGuessProvider is a pass-through wrapper. It delegates every method to the algorithm it holds and adds no behaviour of its own. It exists to give the factory a single return type; whether that justifies the indirection is arguable.
  • Network error handling is coarse. Every failure path returns None, so callers cannot distinguish an unreachable server from a malformed response. Retry and timeout policy is not specified.
  • No persistence. Game state and statistics live in memory only.
  • Non-functional requirements are partly qualitative. "UI clarity" and "stability" were never made measurable and therefore cannot be verified.

Visual direction

The interface borrows from early handheld LCDs: muted olive screens, deep-green ink, crisp square controls, deliberately no neon glow.

The pixel-art arcade cabinet is from the CC0 Arcade Pack by chasersgaming, distributed by Xom Adept via OpenGameArt. Full provenance in assets/ATTRIBUTION.md.


Contributing and security

See CONTRIBUTING.md for setup and quality expectations, and SECURITY.md for disclosure.

License

MIT. See LICENSE.

About

Mastermind and Super-Superhirn built as a full OOA to OOD to OOP case study: layered ports-and-adapters architecture, Strategy and Factory patterns, human vs algorithm vs algorithm modes, pluggable local or remote evaluation. Python, PySide6, UML 2.5.

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages