Compare commits

..

No commits in common. "f715b02e8d058c5409deb2f2b0358cd9f724bc90" and "16e1acc51dd44f8f1ccc7c30af7d22cf22998ec3" have entirely different histories.

7 changed files with 38 additions and 67 deletions

31
DOC.md
View file

@ -14,19 +14,30 @@ Inside of the game, all that is happening is a loop consisting of taking user in
Works the same way, but instead of the player typing guessed sequences, there is a program guessing. Works the same way, but instead of the player typing guessed sequences, there is a program guessing.
The real fun happens inside of the solver, where it makes the guesses and learns from responses. The real fun happens inside of the solver, where it makes the guesses and learns from responses.
The solver keeps track of past guesses with their responses in history.
It also remembers a table with which colors can be on which positions - this starts at all colors possible on all positions.
#### Learning #### Learning
The solver keeps a set of possible sequences. With each new guess-response duo, the solver adds it to the history.
With every response, to a guess, it removes all sequences from the set, which wouldn't generate the same response. Then it goes through all the historical, extracting all possible information from them.
Like this, the solver filters possible sequences, until there is only one left. This is repeated as long as some new information is learnt.
While extracting info from history, it is important to strip guesses from the info already gained,
so they are simplified and information is easier to recognize. That is done by replacing the known colors
in the guess with an unreal color, like *-1* and decreasing the response accordingly.
Extracting information from a guess-response duo is a complicated process,
it has many possibilities, depending on the response.
[A/B] meaning A colors are somewhere else in the sequence, B colors are correct.
- [0/0]: None of the colors from the guess are in the sequence. Now we can erase these colors from the table,
as they are nowhere in the sequence. Of course we must not erase them from the positions we already know they are in,
but were cleared to simplify the guess.
- [X/0]: None of these colors are on the right spot.
- [0/X]: If these colors cannot be on their spot, they can't be in the sequence.
Also if only the colors that can be in those spots are left, they are correct.
- [X/X]: If only the colors that can be in those spots are left, they are correct too.
#### Guessing #### Guessing
We have a set of all possible sequences.
Now we have to pick a guess, which divides them the most equally.
The solver goes through all sequences, not just the remaining possible ones and divides the possible ones by their response.
After that, it gives these guesses a weight, depending on how big is its biggest group at any response.
Finally it picks the guess, which has the least weight - has the smallest biggest remaining sequences count at worst-case.
This is inspired by Donald Knuth and his algorithm to solve Mastermind.
### Response generating ### Response generating
When a guess is entered, all that is done to make a response is comparing that guess to the secret sequence. When a guess is entered, all that is done to make a response is comparing that guess to the secret sequence.

View file

@ -17,11 +17,13 @@ The game has multiple modes:
* **Player mode**: Try your best to guess the random generated sequence of colors. * **Player mode**: Try your best to guess the random generated sequence of colors.
* **Bot mode**: Try to outsmart my bot with a sequence, which he will try to guess. * **Bot mode**: Try to outsmart my bot with a sequence, which he will try to guess.
* **Test mode**: Watch the bot against a random sequence.
## Commandline arguments ## Commandline arguments
* `-n <number>` : Length of guessed sequence * `-n <number>` : Length of guessed sequence
* `-m <number>` : Number of colors to play with * `-m <number>` : Number of colors to play with
* `-p [human/bot]` : Who plays the game * `-p [human/bot]` : Who plays the game
* `-l [y/n]` : Whether you want to see what the bot learns
* `-g [human/random]` : Who generates the guessed sequence (If human plays, this is ignored and the sequence is random) * `-g [human/random]` : Who generates the guessed sequence (If human plays, this is ignored and the sequence is random)
## Usage ## Usage

View file

@ -1,13 +1,10 @@
#include "global.hpp" #include "global.hpp"
#include <cstdlib> #include <cstdlib>
#include <ctime> #include <ctime>
#include <chrono>
using namespace std::chrono;
vector<int> generate(int N, int M) { vector<int> generate(int N, int M) {
srand(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count()); srand((unsigned)time(0));
auto r = vector<int>(N, 0); auto r = vector<int>(N, 0);
for(int n = 0; n < N; n++) for(int n = 0; n < N; n++)

View file

@ -2,6 +2,7 @@
#include <iostream> #include <iostream>
#include <vector> #include <vector>
#include <string> #include <string>
#include <random>
using std::vector; using std::vector;
using std::string; using std::string;

View file

@ -18,7 +18,7 @@ int main(int argc, char* argv[]) {
int N = stoi(get_input("-n", args, "Length of sequence", "5")); int N = stoi(get_input("-n", args, "Length of sequence", "5"));
int M = stoi(get_input("-m", args, "Number of colors", "8")); int M = stoi(get_input("-m", args, "Number of colors", "8"));
string player = get_input("-p", args, string("Who plays [")+HUMAN+"/"+BOT+"]", "bot"); string player = get_input("-p", args, string("Who plays [")+HUMAN+"/"+BOT+"]", "bot");
string gen = player == HUMAN ? RANDOM : get_input("-g", args, "Who generates the sequence", RANDOM); string gen = player == HUMAN ? RANDOM : get_input("-g", args, "Who generates the seque", RANDOM);
bool human_player = player == HUMAN; bool human_player = player == HUMAN;
// Generate the sequence // Generate the sequence

View file

@ -2,7 +2,7 @@
void Solver::generate_set(vector<int> carry) { void Solver::generate_set(vector<int> carry) {
if(carry.size() == N) { if(carry.size() == N) {
possible.insert(carry); possible.push_back(carry);
return; return;
} }
@ -12,76 +12,41 @@ void Solver::generate_set(vector<int> carry) {
carry.pop_back(); carry.pop_back();
} }
} }
Solver::Solver(int _N, int _M) : N(_N), M(_M) { Solver::Solver(int _N, int _M) : N(_N), M(_M) {
generate_set({}); generate_set({});
} }
// TODO
vector<int> Solver::guess() { vector<int> Solver::guess() {
// Optimal first pick is always the same (at least for 5x8) return {};
if(first_pick) {
first_pick = false;
vector<int> pick = {0};
int times = (N-1) / M + 1;
for(int i = 0; i < times; i++)
for(int j = 0; j < M && pick.size() < N; j++)
pick.push_back(j);
return pick;
}
return choose_possible().guess;
} }
void Solver::learn(vector<int> guess, Response response) { void Solver::learn(vector<int> guess, Response response) {
// Eliminating impossible sequences vector<vector<int>> next_possible(0);
set<vector<int>> next_possible; for(int i = 0; i < possible.size(); i++)
for(auto sequence : possible) if(validate(possible[i], guess) == response)
if(validate(sequence, guess) == response) next_possible.push_back(possible[i]);
next_possible.insert(sequence);
possible = next_possible; possible = next_possible;
} }
// TODO
int Solver::get_weight(vector<int> guess) { int Solver::get_weight(vector<int> guess) {
// Bucketing possible sequences by responses return 0;
vector<int> response_count(N*N+N+1, 0);
for(auto sequence : possible) {
Response response = validate(sequence, guess);
response_count[(N+1)*response.somewhere + response.correct]++;
}
// Get size of the fullest bucket
int max = 0;
for(int count : response_count)
if(count > max)
max = count;
// Possible guesses have higher priority
return 2*max - possible.count(guess);
} }
// Choosing next guess
Weighed_guess Solver::minimax(vector<int> carry) { Weighed_guess Solver::minimax(vector<int> carry) {
if(carry.size() == N) if(carry.size() == N)
return {get_weight(carry), carry}; return {get_weight(carry), carry};
// Pick the best of next picks
Weighed_guess best = {-1, {}}; Weighed_guess best = {-1, {}};
for(int col = 0; col < M; col++) { for(int col = 0; col < M; col++) {
carry.push_back(col); carry.push_back(col);
Weighed_guess next = minimax(carry); Weighed_guess next = minimax(carry);
if(best.weight == -1 || next.weight < best.weight) if(next.weight > best.weight)
best = next; best = next;
carry.pop_back(); carry.pop_back();
} }
return best; return best;
} }
Weighed_guess Solver::choose_possible() {
Weighed_guess best = {-1, {}};
for(auto sequence : possible) {
Weighed_guess next = {get_weight(sequence), sequence};
if(best.weight == -1 || next.weight < best.weight)
best = next;
}
return best;
}

View file

@ -1,9 +1,6 @@
#pragma once #pragma once
#include <set>
#include "global.hpp" #include "global.hpp"
using std::set;
// For deciding the best guess // For deciding the best guess
struct Weighed_guess { struct Weighed_guess {
int weight; int weight;
@ -15,8 +12,7 @@ struct Weighed_guess {
// Solving the game // Solving the game
class Solver { class Solver {
int N, M; int N, M;
set<vector<int>> possible; vector<vector<int>> possible = vector<vector<int>>(0);
bool first_pick = true;
public: public:
Solver(int N, int M); Solver(int N, int M);
@ -27,5 +23,4 @@ private:
void generate_set(vector<int> carry); void generate_set(vector<int> carry);
int get_weight(vector<int> guess); int get_weight(vector<int> guess);
Weighed_guess minimax(vector<int> carry); Weighed_guess minimax(vector<int> carry);
Weighed_guess choose_possible();
}; };