[TynesideLUG] Dumping the internal tables of a machine learning penny matching game

Ian Bruntlett ian.bruntlett at gmail.com
Mon Jul 1 16:50:55 UTC 2024


Hi,

In a FB discussion I mentioned patching a machine learning algorithm in an
attempt to learn how the machine learned things.

This code is from the book "Learn C++ by Example", published by Manning.

If you've got an up to date C++ compiler installed, you can compile the
code below (assuming you put it in a file called "bonus_main.cpp" using
this command in a single line:

g++ -g -Wall -Wshadow -Wconversion -std=c++23  -Wl,-Map=bonus_main.map
 bonus_main.cpp   -o bonus_main

The code starts below:
#include <cassert>
#include <concepts>
#include <coroutine>
#include <iostream>
#include <optional>
#include <functional>
#include <map>
#include <memory>
#include <random>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>


// Listing 8.1 Read an optional zero or one
std::optional<int> read_number(std::istream& in)
{
    std::string line;
    std::getline(in, line);
    if (line == "0") {
        return { 0 };
    }
    else if (line == "1") {
        return { 1 };
    }
    return {};
}

// Listing 8.2 A pennies game
void pennies_game()
{
    int player_wins = 0;
    int turns = 0;
    std::mt19937 gen{ std::random_device{}() };
    std::uniform_int_distribution dist(0, 1);

    std::cout << "Select 0 or 1 at random and press enter.\n";
    std::cout << "If the computer predicts your guess it wins.\n";
    while (true)
    {
        const int prediction = dist(gen);

        auto input = read_number(std::cin);
        if (!input)
        {
            break;
        }
        const int player_choice = input.value();

        ++turns;
        std::cout << "You pressed " << player_choice << ", I guessed " <<
prediction << '\n';

        if (player_choice != prediction)
        {
            ++player_wins;
        }
    }
    std::cout << "you win " << player_wins << '\n'
        << "I win " << turns - player_wins << '\n';
}

// Listing 8.3 Three possible choices and outcomes
enum class Choice
{
    Same,
    Change,
    Shrug,
};

// Code by Ian
std::ostream &operator <<(std::ostream &os,const Choice c)
{
  switch (c)
    {
    case Choice::Same: return os << "Same";
    case Choice::Change: return os << "Change";
    case Choice::Shrug: return os << "Shrug";
    default:
      return os << "!!";
    }
}


enum class Outcome
{
    Lose,
    Win,
    Unset,
};

// Code by Ian
std::ostream &operator <<(std::ostream &os,const Outcome o)
{
  switch (o)
    {
    case Outcome::Lose: return os << "Lose";
    case Outcome::Win:  return os << "Win";
    case Outcome::Unset: return os << "Unset";
    default:
      return os << "!!";
    }
}
using state_t = std::tuple<Outcome, Choice, Outcome>;
using last_choices_t = std::pair<Choice, Choice>;

// Code by Ian
std::ostream &operator <<(std::ostream &os,const state_t &st)
{
  os << "( ";
  os << std::get<0>(st) << "," << std::get<1>(st) << "," << std::get<2>(st);
  os << ") ";

  return os;
}

std::ostream &operator <<(std::ostream &os,const last_choices_t &lc)
{
   return os << "(" << lc.first << "," << lc.second << ")";
}

// Listing 8.5 Specializing std::hash for our state tuple
template<>
struct std::hash<state_t>
{
    std::size_t operator()(state_t const& state) const noexcept
    {
        std::size_t h1 = std::hash<Outcome>{}(std::get<0>(state));
        std::size_t h2 = std::hash<Choice>{}(std::get<1>(state));
        std::size_t h3 = std::hash<Outcome>{}(std::get<2>(state));
        return h1 + (h2 << 1) + (h3 << 2);
    }
};

// Listing 8.6 An initial state table
std::unordered_map<state_t, last_choices_t> initial_state()
{
    const auto unset = std::pair<Choice, Choice>{ Choice::Shrug,
Choice::Shrug };
    return {
        { {Outcome::Lose, Choice::Same,   Outcome::Lose}, unset },
        { {Outcome::Lose, Choice::Same,   Outcome::Win},  unset },
        { {Outcome::Lose, Choice::Change, Outcome::Lose}, unset },
        { {Outcome::Lose, Choice::Change, Outcome::Win},  unset },
        { {Outcome::Win,  Choice::Same,   Outcome::Lose}, unset },
        { {Outcome::Win,  Choice::Same,   Outcome::Win},  unset },
        { {Outcome::Win,  Choice::Change, Outcome::Lose}, unset },
        { {Outcome::Win,  Choice::Change, Outcome::Win},  unset },
    };
}

// Listing 8.7 Class to track the game's state
class State
{
    std::unordered_map<state_t, last_choices_t> state_lookup =
initial_state();
public:
  friend std::ostream& operator<< (std::ostream &s, const State &rState);
  // Listing 8.8 Find the choices or return two shrugs
    last_choices_t choices(const state_t& key) const
    {
        if (auto it = state_lookup.find(key); it!=state_lookup.end())
        {
            return it->second;
        }
        else
        {
            return { Choice::Shrug, Choice::Shrug };
        }
    }
    // Listing 8.9 Update choices for valid keys
    void update(const state_t& key, const Choice& turn_changed)
    {
        if (auto it = state_lookup.find(key); it != state_lookup.end())
        {
            const auto [prev2, prev1] = it->second;
            last_choices_t value{ prev1, turn_changed };
            it->second = value;
        }
    }
};

// Code by Ian.
std::ostream& operator<< (std::ostream &s, const State &rState)
{
  s << "State table\n";
  for ( const auto& it : rState.state_lookup ) // we're a friend function,
state is an unordered_map <state_t, last_choices_t>
    {
      s << it.first << " -> "; // state_t is a tuple
      s << it.second;          // last_choices_t is a pair
      s << std::endl;
    }
 return s;
}


// Listing 8.10 Choice from state
Choice prediction_method(const last_choices_t& choices)
{
    if (choices.first == choices.second)
    {
        return choices.first;
    }
    else
    {
        return Choice::Shrug;
    }
}

// Listing 8.11 A mind-reading class
template <std::invocable<> T, typename U>
class MindReader {
    State state_table;

    T generator;
    U distribution;

    int prediction = flip();

    state_t state{Outcome::Unset, Choice::Shrug, Outcome::Unset};
    int previous_go = -1;

    // Listing 8.12 Update the prediction
    bool update_prediction(int player_choice)
    {
        bool guessing = false;
        Choice option = prediction_method(state_table.choices(state));
        switch (option)
        {
        case Choice::Shrug:
            prediction = flip();
            guessing = true;
            break;
        case Choice::Change:
            prediction = player_choice ^ 1;
            break;
        case Choice::Same:
            prediction = player_choice;
            break;
        }
        return guessing;
    }

    int flip()
    {
        return distribution(generator);
    }

public:
    MindReader(T gen, U dis)
        : generator(gen), distribution(dis)
    {
    }

    int get_prediction() const
    {
        return prediction;
    }

    // Listing 8.13 The mind reader's update method
    bool update(int player_choice)
    {
        const Choice turn_changed = player_choice == previous_go ?
Choice::Same : Choice::Change;
        state_table.update(state, turn_changed);

        previous_go = player_choice;
        state = {std::get<2>(state), turn_changed, (player_choice !=
prediction) ? Outcome::Win : Outcome::Lose};

        return update_prediction(player_choice);
    }
  // code by Ian
  void dump(std::ostream &os)
  {
    os << "MindReader()\n";
    os << "this->prediction  = " << this-prediction << ", get_prediction()
= " << get_prediction() <<  '\n';
    os << "this->previous_go = " << this->previous_go << '\n';
    os << "this->state       = " << this->state << '\n';
    os << state_table << '\n';
  }
};

// Listing 8.6 Check we have no hash collisions (and more besides)
void check_properties()
{
    // No bucket clashes
    std::unordered_map<state_t, last_choices_t> states = initial_state();
    for (size_t bucket = 0; bucket < states.bucket_count(); bucket++)
    {
        auto bucket_size = states.bucket_size(bucket);
        assert(bucket_size <= 1);
    }

    {
        MindReader mr([]() { return 0; }, [](auto gen) { return gen(); });
        assert(mr.update(0)); // guesses first
        assert(mr.update(0)); // second is a guess too
    }

    assert(prediction_method({ Choice::Shrug, Choice::Shrug }) ==
Choice::Shrug);
    assert(prediction_method({ Choice::Shrug, Choice::Change }) ==
Choice::Shrug);
    assert(prediction_method({ Choice::Shrug, Choice::Same }) ==
Choice::Shrug);
    assert(prediction_method({ Choice::Change, Choice::Shrug }) ==
Choice::Shrug);
    assert(prediction_method({ Choice::Same, Choice::Shrug }) ==
Choice::Shrug);
    assert(prediction_method({ Choice::Change, Choice::Change }) ==
Choice::Change);
    assert(prediction_method({ Choice::Same, Choice::Same }) ==
Choice::Same);
    assert(prediction_method({ Choice::Change, Choice::Same }) ==
Choice::Shrug);
    assert(prediction_method({ Choice::Same, Choice::Change }) ==
Choice::Shrug);

    State s;
    auto got1 = s.choices({ Outcome::Unset, Choice::Shrug, Outcome::Unset
});
    assert(got1.first == Choice::Shrug);
    assert(got1.second == Choice::Shrug);
    auto got2 = s.choices({ Outcome::Lose, Choice::Same, Outcome::Unset });
    assert(got2.first == Choice::Shrug);
    assert(got2.second == Choice::Shrug);
    auto got3 = s.choices({ Outcome::Win, Choice::Change, Outcome::Unset });
    assert(got3.first == Choice::Shrug);
    assert(got3.second == Choice::Shrug);

    {
        // Listing 6.12 had a RandomBlob we tested, using a lambda to stub
out the random function
        // This always returns 0
        MindReader mr([]() { return 0; }, [](auto gen) { return gen(); });
        assert( mr.update(0)); //flip first two goes
        assert( mr.update(0)); //flip first two goes
        // The random generator always returns zero
        // it guesses first
        // state is
        // lose, same, lose
        // but without two matching next choices
        assert( mr.update(0));
        // now
        // lose, same, lose -> -1 ,lose
        // so when we decide 0 it stops guessing
        // now the state is
        // lose, same, lose -> lose ,lose
        // so it will predict a 0 next
        assert(!mr.update(0));
        assert( mr.get_prediction() == 0);
        assert(!mr.update(0));
        assert( mr.get_prediction() == 0);
        assert(!mr.update(0));
        assert( mr.get_prediction() == 0);
        assert(!mr.update(0));
        assert( mr.get_prediction() == 0);
        assert(!mr.update(0));
        assert( mr.get_prediction() == 0);
    }
}

// Listing 8.14 A mind reading game
void mind_reader()
{
    int turns = 0;
    int player_wins = 0;
    int guessing = 0;

    std::mt19937 gen{ std::random_device{}() };
    std::uniform_int_distribution dist{ 0, 1 };
    MindReader mr(gen, dist);

    std::cout << "Select 0 or 1 at random and press enter.\n";
    std::cout << "If the computer predicts your guess it wins\n";
    std::cout << "and it can now read your mind.\n";
    while (true)
    {
        const int prediction = mr.get_prediction();

// code by Ian
mr.dump (std::cout);

        auto input = read_number(std::cin);
        if (!input)
        {
            break;
        }
        const int player_choice = input.value();

        ++turns;
        std::cout << "You pressed " << player_choice
            << ", I guessed " << prediction << '\n';

        if (player_choice != prediction)
        {
            ++player_wins;
        }
        if (mr.update(player_choice))
        {
            ++guessing;
        }
    }
    std::cout << "you win " << player_wins << '\n'
        << "machine guessed " << guessing << " times" << '\n'
        << "machine won " << (turns - player_wins) << '\n';
}

// Listing 8.18 Customer "deleter"
template<typename Promise>
struct coro_deleter
{
    void operator()(Promise* promise) const noexcept
    {
        auto handle =
std::coroutine_handle<Promise>::from_promise(*promise);
        if (handle)
            handle.destroy();
    }
};

template<typename T>
using promise_ptr = std::unique_ptr<T, coro_deleter<T>>;


// Listing 8.17, 8.20 and 8.21 The coroutine's Task and promise_type
struct Task
{
    // Listing 8.19 Our promise type
    struct promise_type
    {
        std::pair<int, int> choice_and_prediction;

        Task get_return_object()
        {
            return Task(this);
        }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() {}
        std::suspend_always yield_value(std::pair<int, int> got)
        {
            choice_and_prediction = got;
            return {};
        }

        void return_void() { }
    };

    std::pair<int, int> choice_and_prediction() const
    {
        return promise->choice_and_prediction;
    }
    bool done() const
    {
        auto handle =
std::coroutine_handle<promise_type>::from_promise(*promise);
        return handle.done();
    }
    void next()
    {
        auto handle =
std::coroutine_handle<promise_type>::from_promise(*promise);
        handle();
    }
private:
    promise_ptr<promise_type> promise;
    Task(promise_type* p) : promise(p) {}
};

// Listing 8.16 Our first coroutine
Task coroutine_game()
{
    std::mt19937 gen{ std::random_device{}() };
    std::uniform_int_distribution dist{ 0, 1 };
    MindReader mr(gen, dist);

    while (true)
    {
        auto input = read_number(std::cin);
        if (!input)
        {
            co_return;
        }
        int player_choice = input.value();
        co_yield{ player_choice , mr.get_prediction() };
        mr.update(player_choice);
    }
}

// Listing 8.22 A coroutine version of a mind reader
void coroutine_minder_reader()
{
    int turns = 0;
    int player_wins = 0;

    std::cout << "Select 0 or 1 at random and press enter.\n";
    std::cout << "If the computer predicts your guess it wins\nand it can
now read your mind.\n";

    Task game = coroutine_game();

    while (!game.done())
    {
        auto [player_choice, prediction] = game.choice_and_prediction();
        ++turns;
        std::cout << "You pressed " << player_choice << ", I guessed " <<
prediction << '\n';

        if (player_choice != prediction)
        {
            ++player_wins;
        }
        game.next();
    }
    std::cout << "you win " << player_wins << '\n'
        << "machine won " << (turns - player_wins) << '\n';
}

int main()
{
    check_properties();

    // Commented out by Ian
    //  pennies_game();
    // Choose one version of the mind reader (or play both if you want):
    mind_reader();
    //coroutine_minder_reader();
}


More information about the Tyneside mailing list