commit ee7070d6a42082102b8fa58bbba228dd42652bc3 Author: Carl Pearson Date: Sun Sep 7 14:28:42 2025 -0600 initial commit diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6829199 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.5) +project(Candland) +add_executable(candyland candyland.cpp) +set_target_properties(candyland PROPERTIES CXX_STANDARD 20) +set_target_properties(candyland PROPERTIES CXX_STANDARD_REQUIRED TRUE) + +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") + target_compile_options(candyland PRIVATE + -Wall + -Wextra + -Wpedantic + -Wshadow + -Wnon-virtual-dtor + -Wold-style-cast + -Wcast-align + -Wunused + -Woverloaded-virtual + # -Wconversion + -Wnull-dereference + -Wdouble-promotion + -Wformat=2 + ) +endif() diff --git a/candyland.cpp b/candyland.cpp new file mode 100644 index 0000000..5daccea --- /dev/null +++ b/candyland.cpp @@ -0,0 +1,443 @@ + +#include +#include +#include +#include +#include + +constexpr uint8_t START = 0; +constexpr uint8_t RED = 1; +constexpr uint8_t PURPLE = 2; +constexpr uint8_t YELLOW = 3; +constexpr uint8_t BLUE = 4; +constexpr uint8_t ORANGE = 5; +constexpr uint8_t GREEN = 6; +constexpr uint8_t PINK = 7; +constexpr uint8_t LICORICE = 8; +constexpr uint8_t GUMMY_BEAR = 9; +constexpr uint8_t LOLLIPOP = 10; +constexpr uint8_t MINT = 11; +constexpr uint8_t ICE_CREAM = 12; +constexpr uint8_t END = 13; + +using Color = uint16_t; +using BoardIndex = uint8_t; + +struct Space { + uint8_t next[2] = {0,0}; // indices of next possible spaces (0 means none) + Color color : 14; + Color shortcut : 1 = false; // does this space start a shortcut +}; + +struct Card { + uint8_t size; // how many colors (1 or 2) + Color color; +}; + +class BagOfWonder { + std::vector cards; +public: + Card draw() { + auto idx = rand() % cards.size(); + Card c = cards[idx]; + cards.erase(cards.begin() + idx); + return c; + } + + auto empty() const { + return cards.empty(); + } + + void reset() { + cards.clear(); + add(5, {1, 1 << RED}); + add(4, {2, 1 << RED}); + + add(5, {1, 1 << GREEN}); + add(3, {2, 1 << GREEN}); + add(5, {1, 1 << BLUE}); + add(3, {2, 1 << BLUE}); + add(5, {1, 1 << YELLOW}); + add(3, {2, 1 << YELLOW}); + add(5, {1, 1 << ORANGE}); + add(3, {2, 1 << ORANGE}); + add(5, {1, 1 << PURPLE}); + add(3, {2, 1 << PURPLE}); + + add(1, {1, 1 << LICORICE}); + add(1, {1, 1 << GUMMY_BEAR}); + add(1, {1, 1 << LOLLIPOP}); + add(1, {1, 1 << MINT}); + add(1, {1, 1 << ICE_CREAM}); + } + + void add(int n, const Card &c) { + for (int i = 0; i < n; ++i) { + cards.push_back(c); + } + } + + BagOfWonder() { + reset(); + } +}; + +struct State { + BagOfWonder bow; + std::vector players; // player positions + + State(uint8_t nplayers) : players(nplayers, 0) {} + + uint8_t num_players() const { + return players.size(); + } +}; + + +const Space BOARD[83] = { + {{1,0}, 1 << START}, // 0 + + {{2,0}, 1 << RED}, // 1 + {{3,0}, 1 << PURPLE}, + {{4,0}, 1 << YELLOW}, + {{5,0}, 1 << BLUE}, + {{6,0}, 1 << ORANGE}, + {{7,0}, 1 << GREEN}, + + {{8,0}, 1 << RED}, // 7 + {{9,0}, 1 << PURPLE}, + {{10,0}, 1 << YELLOW}, + {{11,0}, 1 << BLUE}, + {{12,0}, 1 << ORANGE}, + {{13,0}, 1 << GREEN}, + + {{14,18}, (1 << PURPLE) | (1 << ORANGE)}, // 13 + {{15,0}, 1 << BLUE}, + {{16,0}, 1 << GREEN}, + {{17,0}, 1 << PURPLE}, + {{22,0}, 1 << BLUE}, + + {{19,0}, 1 << RED}, // 18 + {{20,0}, 1 << YELLOW}, + {{21,0}, 1 << ORANGE}, + {{22,0}, 1 << RED}, + + {{23,0}, 1 << GREEN | 1 << YELLOW}, // 22 + {{24,0}, (1 << LICORICE) | (1 << PINK)}, + {{25,0}, 1 << GREEN}, + {{26,0}, 1 << RED}, + {{27,0}, 1 << PURPLE}, + {{28,0}, 1 << YELLOW}, + {{29,0}, 1 << BLUE}, + {{30,0}, 1 << ORANGE}, + + {{31,0}, 1 << GREEN}, // 30 + {{32,0}, 1 << RED, true /*to 39*/}, + {{33,0}, (1 << GUMMY_BEAR) | (1 << PINK)}, + {{34,0}, 1 << PURPLE}, + {{35,0}, 1 << YELLOW}, + {{36,0}, 1 << BLUE}, + {{37,0}, 1 << ORANGE}, + {{38,0}, 1 << GREEN}, + {{39,0}, 1 << RED}, + {{40,0}, 1 << PURPLE}, + + {{41,0}, 1 << YELLOW}, // 40 + {{42,0}, 1 << BLUE}, + {{43,0}, 1 << ORANGE}, + {{44,0}, (1 << LOLLIPOP) | (1 << PINK)}, + {{45,0}, 1 << RED}, + {{46,0}, 1 << PURPLE}, + {{47,0}, 1 << YELLOW}, + {{48,0}, 1 << BLUE}, + {{49,0}, 1 << ORANGE}, + {{50,0}, 1 << GREEN}, + + {{51,0}, 1 << RED}, // 50 + {{52,0}, (1 << ICE_CREAM) | (1 << PINK)}, + {{53,0}, 1 << PURPLE}, + {{54,0}, 1 << YELLOW}, + {{55,0}, 1 << BLUE}, + {{56,0}, 1 << ORANGE}, + {{57,0}, 1 << GREEN}, + {{58,0}, 1 << RED, true /*to 63*/}, + {{59,0}, 1 << PURPLE}, + {{60,0}, 1 << YELLOW}, + + {{61,0}, 1 << BLUE}, // 60 + {{62,0}, 1 << ORANGE}, + {{63,0}, 1 << GREEN}, + {{64,0}, 1 << RED}, + {{65,0}, 1 << PURPLE}, + {{66,0}, 1 << YELLOW}, + {{67,0}, (1 << MINT) | (1 << PINK)}, + {{68,0}, 1 << BLUE}, + {{69,0}, 1 << ORANGE}, + {{70,0}, 1 << GREEN}, + + {{71,0}, 1 << RED}, // 70 + {{72,0}, 1 << PURPLE}, + {{73,0}, 1 << YELLOW}, + {{74,0}, 1 << BLUE}, + {{75,0}, 1 << ORANGE}, + {{76,0}, 1 << GREEN}, + {{77,0}, 1 << RED}, + {{78,0}, 1 << PURPLE}, + {{79,0}, 1 << YELLOW}, + {{80,0}, 1 << BLUE}, + + {{81,0}, 1 << ORANGE}, // 80 + {{82,0}, 1 << GREEN}, + {{0,0}, 1 << END | 1 << RED | 1 << PURPLE | 1 << YELLOW | 1 << BLUE | 1 << ORANGE | 1 << GREEN}, +}; + +constexpr BoardIndex BOARD_SIZE = sizeof(BOARD) / sizeof(BOARD[0]); + +// faster than an std::set for what we're doing +template +class Set { + std::vector data_; + + public: + + void insert(const T &t) { + if (std::find(data_.begin(), data_.end(), t) == data_.end()) { + data_.push_back(t); + } + } + + void erase(auto it) { + data_.erase(it); + } + + auto begin() const { + return data_.begin(); + } + + auto empty() const { + return data_.empty(); + } +}; + +std::vector reachable_list(BoardIndex pos) { + + static std::map> cache; + + auto it = cache.find(pos); + if (it != cache.end()) { + return it->second; + } + + std::vector result; + Set stack; + + for (auto n : BOARD[pos].next) { + if (n) { + stack.insert(n); + } + } + + while(!stack.empty()) { + const BoardIndex bi = *stack.begin(); + stack.erase(stack.begin()); + result.push_back(bi); + + for (BoardIndex n : BOARD[bi].next) { + if (n) { + stack.insert(n); + } + } + } + + std::sort(result.begin(), result.end()); + + cache[pos] = result; + return result; +} + +struct Result { + int winner; + int landedOnAnother = 0; + int emptyBagOfWonder = 0; + int shortcuts = 0; + int backwards = 0; + int didntMove = 0; + int draws = 0; +}; + +// return the index of the next reachable, unoccupied space of color c after board index bi +int next_reachable(Result &result, const BoardIndex bi, const Color c, State &s) { + + // assemble list of reachable positions from bi + auto reachable = reachable_list(bi); + + // find first tile with that color that's after the current position + // and not occupied + int landedOnAnother = 0; + for (auto j : reachable) { + if (BOARD[j].color & c) { + // check if j is occupied + bool occupied = false; + for (uint8_t pi = 0; pi < s.num_players(); ++pi) { + if (s.players[pi] == j) { + occupied = true; + break; + } + } + if (!occupied) { + // only tally up how many times we landed on another player if we + // actually move + result.landedOnAnother += landedOnAnother; + + // shotcuts go to the same color, so just advance again from a shortcut + if (BOARD[j].shortcut) { + ++result.shortcuts; + return next_reachable(result, j, c, s); + } + return j; + } else { + ++landedOnAnother; + } + } + } + + return bi; +}; + +Result play(State &s) { + + // std::cout << "==== new game ====\n"; + Result result; + + uint8_t curPlayer = 0; + while (true) { + BoardIndex &playerPos = s.players[curPlayer]; + // std::cout << "player " << int(curPlayer) << " @ " << int(playerPos) << "\n"; + + // refill BoW + if (s.bow.empty()) { + ++result.emptyBagOfWonder; + s.bow.reset(); + } + + // draw a card + const Card c = s.bow.draw(); + ++result.draws; + + for (uint8_t i = 0; i < c.size; ++i) { + + // std::cout << "color " << i << ": " << c.color << "\n"; + + switch (c.color) { + // for the candies, put the player 1 step behind + // them, and then advance to the next available pink square + + case 1 << LICORICE: + case 1 << GUMMY_BEAR: + case 1 << LOLLIPOP: + case 1 << MINT: + case 1 << ICE_CREAM: + + for (BoardIndex j = 0; j < BOARD_SIZE; ++j) { + if (BOARD[j].color & c.color) { + if (j == playerPos) { + result.didntMove++; + break; + } + + const BoardIndex before = playerPos; + + // act as if the player was moved one spot behind the target + // pink space, and move to the next free pink space + playerPos = next_reachable(result, j-1, 1 << PINK, s); + // std::cout << "player " << int(curPlayer) << " -> " << int(playerPos) << "\n"; + + if (playerPos < before) { + ++result.backwards; + } + + break; + } + } + break; + + default: + playerPos = next_reachable(result, playerPos, c.color, s); + // std::cout << "player " << int(curPlayer) << " -> " << int(playerPos) << "\n"; + break; + } + + if (BOARD[playerPos].color & (1 << END)) { + result.winner = curPlayer; + return result; + } + } + + curPlayer = (curPlayer + 1) % s.num_players(); + } + + return result; +} + +int main(void) { + + int ngames = 1000000; + int nplayers = 4; + std::vector winCounts(nplayers, 0); + std::vector gameLengths; + + int backwards = 0; + int draws = 0; + int shortcuts = 0; + int didntMove = 0; + int emptyBagOfWonder = 0; + int landedOnAnother = 0; + int mostDraws = 0; + int leastDraws = std::numeric_limits::max(); + for (int i = 0; i < ngames; ++i) { + + if (i % 10000 == 0) { + std::cout << i << "\n"; + } + + State s(nplayers); + Result result = play(s); + backwards += result.backwards; + draws += result.draws; + shortcuts += result.shortcuts; + didntMove += result.didntMove; + emptyBagOfWonder += result.emptyBagOfWonder; + landedOnAnother += result.landedOnAnother; + mostDraws = std::max(mostDraws, result.draws); + leastDraws = std::min(leastDraws, result.draws); + + if (size_t(result.draws) + 1 > gameLengths.size()) { + gameLengths.resize(result.draws + 1); + } + gameLengths[result.draws]++; + + winCounts[result.winner]++; + + // if (result.draws < 7) { + // exit(1); + // } + } + + std::cout << "games played: " << ngames << std::endl; + for (size_t pi = 0; pi < winCounts.size(); ++pi) { + std::cout << "player " << pi << " won: " << winCounts[pi] << std::endl; + } + + std::cout << "total draws: " << draws << std::endl; + std::cout << "backwards moves: " << backwards << std::endl; + std::cout << "shortcuts taken: " << shortcuts << std::endl; + std::cout << "didn't move: " << didntMove << std::endl; + std::cout << "empty BoW: " << emptyBagOfWonder << std::endl; + std::cout << "landed on another: " << landedOnAnother << std::endl; + std::cout << "longest game: " << mostDraws << " draws" << std::endl; + std::cout << "shortest game: " << leastDraws << " draws" << std::endl; + + for (size_t i = 0; i < gameLengths.size(); ++i) { + std::cout << i << ": " << gameLengths[i] << std::endl; + } +} \ No newline at end of file