Skip to content

md-dev970/Nine_Mens_Morris

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Nine Men's Morris

Nine Men's Morris is a desktop implementation of the classic board game built with C++ and Qt Widgets. It supports local player-vs-player matches, player-vs-AI matches, saved games, configurable AI depth, and a full-screen graphical board.

The AI uses minimax search with fail-hard alpha-beta pruning. Game states are represented directly in C++ and the UI is built around Qt's signal/slot system.

Screenshots

Main menu

Game board

Features

  • Local Player vs Player mode.
  • Player vs AI mode.
  • AI difficulty slider from depth 1 to depth 3.
  • Player-first, AI-first, or randomized opening turn in Player vs AI mode.
  • Three-phase Nine Men's Morris rules:
    • placement while each player still has pieces to place;
    • sliding to adjacent slots once all pieces are placed;
    • flying to any empty slot when a player has only three pieces left.
  • Mill detection and mandatory opponent-piece removal.
  • Save, load, continue, and delete saved games.
  • Persistent global settings in src/config/global.ini.
  • Custom Qt stylesheet, fonts, cursor, and board textures.

Technology

  • C++.
  • Qt Widgets.
  • qmake project file: src/Nine_Mens_Morris.pro.
  • INI parsing through the included INIReader/ini.c sources.

The project was developed with a Qt 6 MinGW kit. The .pro file also includes the usual Qt 4+ widgets guard:

QT = core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

Repository Layout

.
|-- README.md
|-- screenshots/
|-- src/
|   |-- Nine_Mens_Morris.pro
|   |-- main.cpp
|   |-- mainwindow.*
|   |-- gameboard.*
|   |-- game.*
|   |-- state.*
|   |-- move.*
|   |-- player.*
|   |-- mainmenuwidget.*
|   |-- playmenu.*
|   |-- pausemenu.*
|   |-- historywidget.*
|   |-- settingswidget.*
|   |-- gamecard.*
|   |-- confirmationdialog.*
|   |-- mainconfig.*
|   |-- INIReader.* / ini.*
|   |-- config/
|   |-- fonts/
|   |-- media/
|   |-- savegames/
|   `-- style/

Architecture

The app is split into a small model layer, a Qt UI layer, and a simple persistence/config layer.

flowchart TD
    App["main.cpp"] --> Window["MainWindow"]
    Window --> Stack["QStackedWidget screens"]
    Stack --> MainMenu["MainMenuWidget"]
    Stack --> PlayMenu["PlayMenu"]
    Stack --> Board["GameBoard"]
    Stack --> Pause["PauseMenu"]
    Stack --> History["HistoryWidget"]
    Stack --> Settings["SettingsWidget"]
    Board --> Game["Game"]
    Board --> AI["AIPlayer"]
    Game --> State["State"]
    State --> Move["Move"]
    AI --> State
    History --> Saves["src/savegames/*.txt"]
    Game --> Saves
    Settings --> Config["src/config/global.ini"]
Loading

Entry Point

src/main.cpp creates the QApplication, loads the custom font, loads src/style/style.qss, installs the custom wooden cursor, creates MainWindow, and shows it full-screen.

Screen Flow

MainWindow owns a QStackedWidget and switches between screens:

  • MainMenuWidget: Play, Settings, History, Quit.
  • PlayMenu: game mode, AI difficulty, and first-player selection.
  • GameBoard: the actual board, input handling, rendering, and pause button.
  • PauseMenu: resume, restart, save, load, or return to the main menu.
  • HistoryWidget: saved-game cards with continue/delete actions.
  • SettingsWidget: sound and language controls.
  • ConfirmationDialog: restart/quit confirmation prompts.

The widgets communicate through Qt signals and slots. For example, the play menu emits startButtonClicked(GameMode, int, int), which MainWindow::startGame receives and forwards into GameBoard::setGameSettings.

Game Model

The model is centered on four classes:

  • Move: a compact command object containing source coordinates, destination coordinates, optional removal coordinates, and the color making the move. The sentinel coordinate 3 means "not applicable" for source/removal.
  • State: the current board, piece counts, current turn, mill locking, win detection, child-state generation, evaluation, minimax, and alpha-beta pruning.
  • Game: a wrapper around the current State, the state history, move count, game mode, selected player color, difficulty, and save/load routines.
  • Player, HumanPlayer, AIPlayer: player interfaces. AIPlayer asks the current state for the best next state and converts that state difference back into a Move.

Board Representation

The board is stored as:

Cell board[3][8];

The first index is the ring:

  • 0: outer square.
  • 1: middle square.
  • 2: inner square.

The second index is the position around that ring. The implementation numbers positions clockwise around each square:

0 -------- 1 -------- 2
|          |          |
7          |          3
|          |          |
6 -------- 5 -------- 4

Odd positions (1, 3, 5, 7) are the side midpoints. They are connected both around the ring and across rings. Even positions are square corners and only connect around their own ring.

Cells are stored with the Cell enum from src/init.hpp:

  • WHITE / BLACK: regular pieces.
  • WHITE_LOCKED / BLACK_LOCKED: pieces currently part of a mill.
  • EMPTY: empty slot.

The locked values let the engine distinguish pieces that are currently in a mill while still treating them as owned by the same player through State::isLocked.

Gameplay Implementation

Turn Flow

GameBoard::mousePressEvent maps a click to board coordinates and then handles the current phase:

  1. Ignore input if the AI is thinking/playing or the game already has a winner.
  2. Build a Move for the current player.
  3. In placement phase, place a piece on an empty slot.
  4. In movement phase, first select one of the current player's pieces, then select a valid destination.
  5. If a mill is formed, set awaitRemove and wait for the player to click an opponent piece.
  6. Apply the move through Game::makeMove.
  7. Check for a winner.
  8. If the mode is Player vs AI, call GameBoard::applyAIMove.

Placement, Sliding, and Flying

The game uses moveCount < 18 to determine the placement phase. Since each player has nine pieces, the first eighteen moves are placements.

After placement:

  • A player with more than three pieces may slide only to adjacent empty slots.
  • A player with exactly three pieces may fly to any empty slot.

The same movement rules are mirrored in State::createChildList, which is used by the AI to enumerate legal next states.

Mill Detection

Mills are checked in two forms:

  • Horizontal mills around a ring, starting from even positions.
  • Vertical mills across rings, using odd positions.

When State::update applies a move, it first unlocks broken mills for the player who just moved, then locks newly created mills by converting pieces from WHITE/BLACK to WHITE_LOCKED/BLACK_LOCKED.

When a new mill is detected by the UI or generated by the AI search, the move includes an opponent piece to remove.

Win Detection

State::checkWin returns:

  • BLACK if white has only two pieces left.
  • WHITE if black has only two pieces left.
  • EMPTY if the game is still in placement, or if either player has exactly three pieces and can still fly.
  • Otherwise, the opponent wins if the current player has no legal adjacent movement.

AI Algorithm

AIPlayer::getNextMove calls:

State next = current.alphaBeta(diff, current.getTurn());

The selected difficulty is used directly as the search depth. The returned best child state is compared against the current state with State::getMove to recover the concrete Move.

Child-State Generation

State::createChildList generates legal successors for the current turn:

  1. Placement phase: every empty slot is a possible destination.
  2. Flying phase: if the current player has three pieces left, every owned piece can move to any empty slot.
  3. Sliding phase: pieces can move only to adjacent empty slots.
  4. If a generated move forms a mill, the generator creates one child state for each removable opponent piece.

Evaluation Function

State::evaluate(Cell maxTurn) assigns:

  • positive infinity if maxTurn has won;
  • negative infinity if the opponent has won;
  • otherwise, a heuristic score based on:
    • available mill lines for maxTurn;
    • available mill lines for the opponent;
    • material balance, weighted by 100 * pawnDifference.

In simplified form:

score = availableLines(maxTurn) - availableLines(opponent)
score += 100 * (pawnsLeft(maxTurn) - pawnsLeft(opponent))

Alpha-Beta Pruning

State::alphaBeta is a fail-hard alpha-beta minimax search:

  • maximizing levels keep the highest score found so far and prune when value >= beta;
  • minimizing levels keep the lowest score found so far and prune when value <= alpha;
  • leaf nodes and terminal states are evaluated with State::evaluate.

The plain State::minMax implementation is still present for comparison/debugging, but the AI uses alphaBeta.

Persistence and Configuration

Saved Games

Saved games are stored under:

src/savegames/

Game::save writes a binary stream containing:

  1. game mode;
  2. player color;
  3. AI difficulty;
  4. move count;
  5. every State in the state history.

Game::load reads the same stream back, restores the latest state as the current board, checks the winner, and restores the turn.

Save names are generated as:

savegame_<saveCount>.txt

The counter comes from MainConfig.

Global Settings

Global settings live in:

src/config/global.ini

The current settings are:

  • language: currently stored and surfaced in the settings UI.
  • sound: updated by the settings slider.
  • saveCount: incremented whenever a new save is created.

MainConfig loads these values on startup and writes them back when the global config object is destroyed.

How to Run

Option 1: Qt Creator

  1. Install Qt with a Desktop kit, for example Qt 6.x with MinGW on Windows.
  2. Open src/Nine_Mens_Morris.pro in Qt Creator.
  3. Configure the project with your Desktop kit.
  4. Make sure the run working directory can resolve the runtime asset folders.
  5. Build and run.

The bootstrap code loads most assets relative to src:

./fonts/vinque-rg.otf
./style/style.qss
./media/wooden_cursor.png
./config/global.ini
./savegames/

Some board assets are currently referenced from GameBoard with ../../media/.... If the board texture or pause icon does not appear, adjust the Qt Creator working directory, copy the media folder next to the expected relative path, or normalize the asset paths in code before packaging.

Option 2: Command Line

Run these commands from the src directory after adding your Qt kit's qmake and compiler tools to PATH:

cd src
qmake Nine_Mens_Morris.pro
mingw32-make

Then launch the generated executable from a working directory that can see fonts, style, media, config, and savegames. Because a few board images use ../../media/..., verify the board texture and pause icon after launching.

The exact output directory depends on your Qt kit and qmake configuration. Qt Creator commonly creates a sibling or nested build directory named like:

build/Desktop_Qt_<version>_<compiler>-<configuration>/

Development Notes

  • The application is path-sensitive. If assets fail to load, check the run working directory and the mixed ./media / ../../media references first.
  • The game currently uses qmake rather than CMake.
  • The repository includes generated build artifacts under src/build/; these are not required to understand or edit the source.
  • Saved games are binary despite the .txt extension.
  • There is no automated test suite in the repository yet.

Known Limitations

  • The settings screen stores language and sound values, but language switching and audio playback are not fully implemented in the visible UI flow.
  • Save files are binary dumps of in-memory structs, so they are best treated as version-specific and compiler-specific.
  • The app is designed around full-screen desktop play and fixed board coordinates.
  • Some debug logging remains in the implementation through qDebug and std::cout.

Quick Code Map

  • src/main.cpp: application bootstrap, font/style/cursor loading.
  • src/mainwindow.*: screen ownership, navigation, and high-level actions.
  • src/gameboard.*: board rendering, mouse interaction, game phase handling, and AI move application.
  • src/game.*: current state, history, save/load, mode/difficulty/player settings.
  • src/state.*: board state, legal move generation, mill locking, win detection, evaluation, minimax, alpha-beta pruning.
  • src/move.*: move data object.
  • src/player.*: human/AI player abstractions.
  • src/mainconfig.*: INI-backed settings and save counter.
  • src/historywidget.* and src/gamecard.*: save listing, load, and delete UI.
  • src/playmenu.*, src/pausemenu.*, src/settingswidget.*, src/mainmenuwidget.*: menu widgets.

About

Desktop implementation of the classic board game "Nine men's Morris" built with C++ and Qt Widgets.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors