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.
Engineering documentation (PDF) · Architecture · Quick start · Modes
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/.
| 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 |
- 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.
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
| 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 |
Components and the interfaces between them. Every arrow crosses a named port. Full resolution: component-diagram.pdf
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.
| 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 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.
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.
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.
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.
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.
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.pyFor development, including tests, linting and the mock server:
python -m pip install -r requirements-dev.txtThe bundled artwork has to travel with the executable:
pyinstaller --noconfirm --windowed --name SuperHirn --add-data "assets;assets" main.pyOutput lands in dist/SuperHirn/.
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.pyIn 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.
ruff check .
python -m pytest
python -m pytest --cov --cov-report=term-missingGitHub 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 |
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
Written down deliberately. This was a course project under a fixed deadline, not a finished product.
IEvaluationProvidercarries more than evaluation. Next toevaluateit exposesset_feedback,get_feedback_if_availableanduses_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_guessis optional. It raisesNotImplementedErrorin the base and onlyUIGuessProviderimplements it, so not every adapter is substitutable through the full interface.AIGuessProvideris 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.
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.
See CONTRIBUTING.md for setup and quality expectations, and SECURITY.md for disclosure.
MIT. See LICENSE.