From 574607a5ebc8e5ca0cb1f7d4987814131ef27b8a Mon Sep 17 00:00:00 2001 From: Geoffrey Daniels Date: Tue, 19 Sep 2023 22:17:45 +0100 Subject: [PATCH] Adding mastermind solver. --- README.md | 1 + source/game/mastermind | 183 +++++++++++++++++++++++++++++++++ tests/game/mastermind.test.cpp | 88 ++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 source/game/mastermind create mode 100644 tests/game/mastermind.test.cpp 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); + } +} +