initial commit

This commit is contained in:
2025-09-07 14:28:42 -06:00
commit ee7070d6a4
2 changed files with 466 additions and 0 deletions

23
CMakeLists.txt Normal file
View File

@@ -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()

443
candyland.cpp Normal file
View File

@@ -0,0 +1,443 @@
#include <algorithm>
#include <iostream>
#include <map>
#include <numeric>
#include <vector>
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<Card> 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<BoardIndex> 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 <typename T>
class Set {
std::vector<T> 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<BoardIndex> reachable_list(BoardIndex pos) {
static std::map<BoardIndex, std::vector<BoardIndex>> cache;
auto it = cache.find(pos);
if (it != cache.end()) {
return it->second;
}
std::vector<BoardIndex> result;
Set<BoardIndex> 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<int> winCounts(nplayers, 0);
std::vector<int> 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<int>::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;
}
}