diff --git a/README.md b/README.md
index 37ec54a..5fb7181 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,7 @@ The current classes are as described below:
| [execution](source/execution) | [triple_buffer](source/execution/triple_buffer) | Lockless triple buffer interface to three buffers. | :heavy_check_mark: |
| [file/archive](source/file/archive) | [tar](source/file/archive/tar) | Tar format archive reader and writer. | :construction: |
| [file/text](source/file/text) | [json](source/file/text/json) | A small json parser and composer. | :construction: |
+| [game](source/game) | [mastermind](source/game/mastermind) | An implementation of Donald Knuth's algorithm to solve the mastermind game in five moves or less. | :construction: |
| [game](source/game) | [sudoku](source/game/sudoku) | A sudoku solver for standard 9x9 grids. | :construction: |
| [game](source/game) | [tic_tac_toe](source/game/tic_tac_toe) | Solver for the game tic\-tac\-toe on a 3x3 board. | :construction: |
| [hash](source/hash) | [crc](source/hash/crc) | An implementation of the crc hashing function for 8, 16, 32, and 64 bits. | :heavy_check_mark: |
diff --git a/source/game/mastermind b/source/game/mastermind
new file mode 100644
index 0000000..6d843ee
--- /dev/null
+++ b/source/game/mastermind
@@ -0,0 +1,183 @@
+/*
+Copyright (C) 2018-2023 Geoffrey Daniels. https://gpdaniels.com/
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License only.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+#ifndef GTL_GAME_MASTERMIND_HPP
+#define GTL_GAME_MASTERMIND_HPP
+
+// Summary: An implementation of Donald Knuth's algorithm to solve the mastermind game in five moves or less. [wip]
+
+#if defined(_MSC_VER)
+#pragma warning(push, 0)
+#endif
+
+#include
+#include
+#include
+#include
+#include
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+namespace gtl {
+ template
+ class mastermind final {
+ private:
+ static_assert(code_length > 0);
+ static_assert(code_base > 0);
+
+ private:
+ class score_type {
+ public:
+ unsigned int correct;
+ unsigned int close;
+ public:
+ bool operator==(const score_type& other) const {
+ return ((this->correct == other.correct) && (this->close == other.close));
+ }
+ };
+
+ public:
+ static unsigned int solve(
+ const std::array& code,
+ const std::function& guess,
+ unsigned int correct, unsigned int close)>& callback = {}
+ ) {
+ // Generate all possible codes.
+ std::vector> unguessed_codes;
+ unguessed_codes.push_back(std::array());
+ do {
+ unguessed_codes.push_back(unguessed_codes.back());
+ for (unsigned int i = 0; i < code_length; ++i) {
+ unguessed_codes.back()[code_length - 1 - i] = (unguessed_codes.back()[code_length - 1 - i] + 1) % code_base;
+ if (unguessed_codes.back()[code_length - 1 - i] != 0) {
+ break;
+ }
+ }
+ } while (unguessed_codes.back() != unguessed_codes.front());
+ unguessed_codes.pop_back();
+ std::vector> possible_codes = unguessed_codes;
+ // Create an initial guess.
+ std::array guess = {};
+ for (unsigned int i = 0; i < code_length; ++i) {
+ guess[i] = (i >= (code_length / 2));
+ }
+ // Play the game.
+ unsigned int turns = 0;
+ do {
+ // Score the current guess.
+ score_type score = evaluate(guess, code);
+ // Increment the guess counter.
+ ++turns;
+ // Print the state.
+ if (callback) {
+ callback(turns - 1, guess, score.correct, score.close);
+ }
+ // Check if the guess is correct.
+ if (score.correct == code_length) {
+ break;
+ }
+ // If the guess is not correct:
+ // Remove the guess from the unguessed set.
+ unguessed_codes.erase(std::find(unguessed_codes.begin(), unguessed_codes.end(), guess));
+ // Remove the guess from the possible set, and remove possible codes that do not match the returned score.
+ possible_codes.erase(
+ std::remove_if(
+ possible_codes.begin(),
+ possible_codes.end(),
+ [guess, score](const std::array& possible_code)->bool{
+ if (guess == possible_code) {
+ return true;
+ }
+ score_type possible = evaluate(guess, possible_code);
+ return ((score.correct != possible.correct) || (score.close != possible.close));
+ }
+ ),
+ possible_codes.end()
+ );
+ // If there is only one possible code left, select that.
+ if (possible_codes.size() == 1) {
+ guess = possible_codes.back();
+ continue;
+ }
+ // Otherwise select a new guess using minimax.
+ std::vector scores(unguessed_codes.size(), 0);
+ for (unsigned int i = 0; i < unguessed_codes.size(); ++i) {
+ struct score_hash {
+ std::size_t operator()(const score_type& key) const {
+ return key.correct * code_length + key.close;
+ }
+ };
+ // Calculate the score/pegs of each unguessed code as if a possible code was the code.
+ std::unordered_map score_map;
+ for (unsigned int j = 0; j < possible_codes.size(); ++j) {
+ ++score_map[evaluate(unguessed_codes[i], possible_codes[j])];
+ }
+ // From these scores select the maximum, this is the worst case number of possible codes for a given unguessed code.
+ scores[i] = std::max_element(score_map.begin(), score_map.end(),
+ [](const std::pair& lhs, const std::pair& rhs)->bool{
+ return lhs.second < rhs.second;
+ }
+ )->second;
+ }
+ // Select the minimum of the maximums to get the best guess.
+ std::vector::iterator min_element = std::min_element(scores.begin(), scores.end());
+ guess = unguessed_codes[std::distance(scores.begin(), min_element)];
+ // Check to see if there is a possible code with the same score, if so, prefer that.
+ const unsigned int min_score = *min_element;
+ for (unsigned int i = 0; i < unguessed_codes.size(); ++i) {
+ if (scores[i] == min_score) {
+ if (std::find(possible_codes.begin(), possible_codes.end(), unguessed_codes[i]) != possible_codes.end()) {
+ guess = unguessed_codes[i];
+ break;
+ }
+ }
+ }
+ } while (true);
+ return turns;
+ }
+
+ private:
+ static score_type evaluate(const std::array& guess, const std::array& code) {
+ unsigned int correct = 0;
+ unsigned int close = 0;
+ std::array consumed = {};
+ for (unsigned int i = 0; i < code_length; ++i) {
+ if (guess[i] == code[i]) {
+ consumed[i] = true;
+ ++correct;
+ }
+ }
+ for (unsigned int i = 0; i < code_length; ++i) {
+ if (guess[i] != code[i]) {
+ for (unsigned int j = 0; j < code_length; ++j) {
+ if ((guess[i] == code[j]) && (!consumed[j])) {
+ consumed[j] = true;
+ ++close;
+ break;
+ }
+ }
+ }
+ }
+ return {correct, close};
+ }
+ };
+}
+
+#endif // GTL_GAME_MASTERMIND_HPP
diff --git a/tests/game/mastermind.test.cpp b/tests/game/mastermind.test.cpp
new file mode 100644
index 0000000..f2b6301
--- /dev/null
+++ b/tests/game/mastermind.test.cpp
@@ -0,0 +1,88 @@
+/*
+Copyright (C) 2018-2023 Geoffrey Daniels. https://gpdaniels.com/
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License only.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include
+#include
+#include
+#include
+
+#include
+
+#if defined(_MSC_VER)
+# pragma warning(push, 0)
+#endif
+
+#include
+#include
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
+
+TEST(mastermind, constructor, empty) {
+ constexpr static const unsigned int code_length = 4;
+ constexpr static const unsigned int code_base = 6;
+ gtl::mastermind mastermind;
+ testbench::do_not_optimise_away(mastermind);
+}
+
+TEST(mastermind, function, solve) {
+ constexpr static const unsigned int code_length = 4;
+ constexpr static const unsigned int code_base = 6;
+ {
+ unsigned int turns = gtl::mastermind::solve({1,2,3,4});
+ REQUIRE(turns <= 5);
+ }
+ {
+ unsigned int turns = gtl::mastermind::solve({5,4,3,2}, [](unsigned int turn, const std::array& guess, unsigned int correct, unsigned int close){
+ PRINT("GUESS %d: %d %d %d %d ==> %d %d\n", turn, guess[0], guess[1], guess[2], guess[3], correct, close);
+ });
+ REQUIRE(turns <= 5);
+ }
+}
+
+TEST(mastermind, evaluate, all) {
+ constexpr static const unsigned int code_length = 4;
+ constexpr static const unsigned int code_base = 6;
+ // Generate all possible codes.
+ std::vector> all_codes;
+ all_codes.push_back(std::array());
+ do {
+ all_codes.push_back(all_codes.back());
+ for (unsigned int i = 0; i < code_length; ++i) {
+ all_codes.back()[code_length - 1 - i] = (all_codes.back()[code_length - 1 - i] + 1) % code_base;
+ if (all_codes.back()[code_length - 1 - i] != 0) {
+ break;
+ }
+ }
+ } while (all_codes.back() != all_codes.front());
+ all_codes.pop_back();
+
+#if !defined(NDEBUG)
+ // Sample the full set as running the full set is slow.
+ std::vector> test_codes;
+ std::sample(all_codes.begin(), all_codes.end(), std::back_inserter(test_codes), 10, std::mt19937{std::random_device{}()});
+#else
+ std::vector>& test_codes = all_codes;
+#endif
+
+ // Solve codes.
+ for (unsigned int i = 0; i < test_codes.size(); ++i) {
+ unsigned int turns = gtl::mastermind::solve(test_codes[i]);
+ REQUIRE(turns <= 5, "Code %d %d %d %d took %d turns.", test_codes[i][0], test_codes[i][1], test_codes[i][2], test_codes[i][3], turns);
+ }
+}
+