Compare commits

..

10 commits

7 changed files with 67 additions and 38 deletions

31
DOC.md
View file

@ -14,30 +14,19 @@ 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.
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
With each new guess-response duo, the solver adds it to the history.
Then it goes through all the historical, extracting all possible information from them.
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.
The solver keeps a set of possible sequences.
With every response, to a guess, it removes all sequences from the set, which wouldn't generate the same response.
Like this, the solver filters possible sequences, until there is only one left.
#### 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
When a guess is entered, all that is done to make a response is comparing that guess to the secret sequence.

View file

@ -17,13 +17,11 @@ The game has multiple modes:
* **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.
* **Test mode**: Watch the bot against a random sequence.
## Commandline arguments
* `-n <number>` : Length of guessed sequence
* `-m <number>` : Number of colors to play with
* `-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)
## Usage

View file

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

View file

@ -2,7 +2,6 @@
#include <iostream>
#include <vector>
#include <string>
#include <random>
using std::vector;
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 M = stoi(get_input("-m", args, "Number of colors", "8"));
string player = get_input("-p", args, string("Who plays [")+HUMAN+"/"+BOT+"]", "bot");
string gen = player == HUMAN ? RANDOM : get_input("-g", args, "Who generates the seque", RANDOM);
string gen = player == HUMAN ? RANDOM : get_input("-g", args, "Who generates the sequence", RANDOM);
bool human_player = player == HUMAN;
// Generate the sequence

View file

@ -2,7 +2,7 @@
void Solver::generate_set(vector<int> carry) {
if(carry.size() == N) {
possible.push_back(carry);
possible.insert(carry);
return;
}
@ -12,41 +12,76 @@ void Solver::generate_set(vector<int> carry) {
carry.pop_back();
}
}
Solver::Solver(int _N, int _M) : N(_N), M(_M) {
generate_set({});
}
// TODO
vector<int> Solver::guess() {
return {};
// Optimal first pick is always the same (at least for 5x8)
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) {
vector<vector<int>> next_possible(0);
for(int i = 0; i < possible.size(); i++)
if(validate(possible[i], guess) == response)
next_possible.push_back(possible[i]);
// Eliminating impossible sequences
set<vector<int>> next_possible;
for(auto sequence : possible)
if(validate(sequence, guess) == response)
next_possible.insert(sequence);
possible = next_possible;
}
// TODO
int Solver::get_weight(vector<int> guess) {
return 0;
// Bucketing possible sequences by responses
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) {
if(carry.size() == N)
return {get_weight(carry), carry};
// Pick the best of next picks
Weighed_guess best = {-1, {}};
for(int col = 0; col < M; col++) {
carry.push_back(col);
Weighed_guess next = minimax(carry);
if(next.weight > best.weight)
if(best.weight == -1 || next.weight < best.weight)
best = next;
carry.pop_back();
}
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,6 +1,9 @@
#pragma once
#include <set>
#include "global.hpp"
using std::set;
// For deciding the best guess
struct Weighed_guess {
int weight;
@ -12,7 +15,8 @@ struct Weighed_guess {
// Solving the game
class Solver {
int N, M;
vector<vector<int>> possible = vector<vector<int>>(0);
set<vector<int>> possible;
bool first_pick = true;
public:
Solver(int N, int M);
@ -23,4 +27,5 @@ private:
void generate_set(vector<int> carry);
int get_weight(vector<int> guess);
Weighed_guess minimax(vector<int> carry);
Weighed_guess choose_possible();
};