Compare commits
10 commits
16e1acc51d
...
f715b02e8d
Author | SHA1 | Date | |
---|---|---|---|
f715b02e8d | |||
c55dbee834 | |||
213209243e | |||
5dbaa7444f | |||
10c52ebec9 | |||
c73d5c9ba4 | |||
604f229cd8 | |||
c095843041 | |||
7b591bf2aa | |||
2dd5ef9b99 |
7 changed files with 67 additions and 38 deletions
31
DOC.md
31
DOC.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
5
gen.cpp
5
gen.cpp
|
@ -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++)
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <random>
|
||||
|
||||
using std::vector;
|
||||
using std::string;
|
||||
|
|
2
main.cpp
2
main.cpp
|
@ -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
|
||||
|
|
57
solver.cpp
57
solver.cpp
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue