Last active
February 14, 2022 13:19
-
-
Save victorliu/8265574 to your computer and use it in GitHub Desktop.
Hanabi card game simulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Hanabi simulator | |
================ | |
To use, subclass struct hanabi_player like so: | |
struct custom_data{ | |
// whatever other state needed by the custom hanabi_player | |
}; | |
struct hanabi_player *player = (struct hanabi_player*)malloc(sizeof(struct hanabi_player) + sizeof(struct custom_data)); | |
struct custom_data *data = (struct custom_data *)(player+1); | |
Then, implement new callbacks for the player. The current default draw callback follows are existing convention of drawing into the back. To implement non-time-invariant actions, use the info callback and store the context of the game state. | |
currently struct hanabi_public is simply populated with all game state as opposed to only the publicly available information (because I'm lazy). | |
*/ | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <assert.h> | |
/* | |
Colors: (alphabetical order, rainbow at end) | |
BGRWY* | |
Numbers: | |
12345 | |
Cards: | |
(30 distinct, | |
60 total counting | |
multiplicity) | |
index: | |
1B 2B 3B 4B 5B 0 1 2 3 4 | |
1G 2G 3G 4G 5G 5 6 7 8 9 | |
1R 2R 3R 4R 5R 10 11 12 13 14 | |
1W 2W 3W 4W 5W 15 16 17 18 19 | |
1Y 2Y 3Y 4Y 5Y 20 21 22 23 24 | |
1* 2* 3* 4* 5* 25 26 27 28 29 | |
Game locations: | |
6 stacks for build piles | |
30 stacks for discard piles | |
N hands of H slots for hands | |
*/ | |
#define DECK_SIZE 60 | |
#define RAINBOW_COLOR_IDX 5 | |
// knowledge: | |
// bit field of what properties of the card can be | |
// low 5 bits: the number of the card | |
// next 6 bits: the color of the card | |
#define KNOW_MASK 0x7FF | |
#define KNOW_NUM_MASK 0x01F | |
#define KNOW_NUM_OFFSET 0 | |
#define KNOW_COL_MASK 0x7E0 | |
#define KNOW_COL_OFFSET 5 | |
#define KNOW_RAINBOW_BIT 0x400 | |
static const int handsize[6] = {-1,-1,-1,5,4,4}; | |
/* Convert a card color and number to its coded index | |
* colors: B = 0 | |
* G = 1 | |
* R = 2 | |
* W = 3 | |
* Y = 4 | |
* rainbow = 5 | |
* numbers are 1 to 5 | |
*/ | |
int cn2i(int color, int number){ | |
return color*5+(number-1); | |
} | |
/* convert a given card index i to its color and number */ | |
void i2cn(int i, int *color, int *number){ | |
*color = i/5; | |
*number = (i%5) + 1; | |
} | |
/* a player's hand | |
* ncards is number of cards currently in hand | |
* card[i] is the i'th card's card index | |
* know[i] is the information bits about the i-th card (see the KNOW_* defines) | |
* a card starts off with all bits set (maximally ignorant, all possilities valid) | |
* each bit basically represents a thing the card can be | |
*/ | |
struct hanabi_hand{ | |
int ncards; | |
int card[5]; | |
int know[5]; | |
}; | |
/* print a card index in readable form (number char, then color char)*/ | |
void fprintcard(FILE *fp, int card){ | |
static const char colchar[6] = {'B','G','R','W','Y','*'}; | |
int col, num; | |
i2cn(card, &col, &num); | |
fputc('0'+num, fp); | |
fputc(colchar[col], fp); | |
} | |
/* note: we never "remove" cards from the deck array. We deal by just reading the next appropriate entry */ | |
struct hanabi_game{ | |
int nplayers; | |
int active_player; | |
int ninfo, nbomb, ndeck; // number of information tokens, bomb tokens, and cards remaining in deck | |
// cards in completion piles, value is current top card number (default 0) | |
int pile[6]; | |
// cards in discard, each entry is the number of that card index that has been discarded | |
int discard[30]; | |
// cards in deck (the card index of each card in the deck) | |
int deck[60]; | |
}; | |
// This is filled in with only publicly available knowledge | |
struct hanabi_public{ | |
struct hanabi_game game; | |
struct hanabi_hand hand[5]; // 0 is current active player, 1 is next in turn order, etc. | |
}; | |
/* print the public data structure */ | |
void fprintpub(FILE *fp, const struct hanabi_public *pub){ | |
int i, j; | |
fprintf(fp, "Piles: B G R W Y *\n"); | |
fprintf(fp, " %d %d %d %d %d %d\n", | |
pub->game.pile[0], | |
pub->game.pile[1], | |
pub->game.pile[2], | |
pub->game.pile[3], | |
pub->game.pile[4], | |
pub->game.pile[5] | |
); | |
fprintf(fp, "Discard:\n"); | |
for(i = 1; i <= 5; ++i){ | |
int col; | |
fprintf(fp, " %d", i); | |
for(col = 0; col < 6; ++col){ | |
fprintf(fp, " %d", pub->game.discard[cn2i(col,i)]); | |
} | |
fprintf(fp, "\n"); | |
} | |
fprintf(fp, "Cards remaining in deck: %d\n", pub->game.ndeck); | |
fprintf(fp, "Information tokens remaining: %d\n", pub->game.ninfo); | |
fprintf(fp, "Bomb tokens acquired: %d\n", pub->game.nbomb); | |
fprintf(fp, "Current hand:\n"); | |
for(i = 0; i < pub->hand[0].ncards; ++i){ | |
fprintf(fp, " %c%c%c%c%c ", | |
(pub->hand[0].know[i] & (1 << (0+KNOW_NUM_OFFSET)) ? '1' : ' '), | |
(pub->hand[0].know[i] & (1 << (1+KNOW_NUM_OFFSET)) ? '2' : ' '), | |
(pub->hand[0].know[i] & (1 << (2+KNOW_NUM_OFFSET)) ? '3' : ' '), | |
(pub->hand[0].know[i] & (1 << (3+KNOW_NUM_OFFSET)) ? '4' : ' '), | |
(pub->hand[0].know[i] & (1 << (4+KNOW_NUM_OFFSET)) ? '5' : ' ') | |
); | |
} | |
fprintf(fp, "\n"); | |
for(i = 0; i < pub->hand[0].ncards; ++i){ | |
fprintf(fp, " %c%c%c%c%c%c", | |
(pub->hand[0].know[i] & (1 << (0+KNOW_COL_OFFSET)) ? 'B' : ' '), | |
(pub->hand[0].know[i] & (1 << (1+KNOW_COL_OFFSET)) ? 'G' : ' '), | |
(pub->hand[0].know[i] & (1 << (2+KNOW_COL_OFFSET)) ? 'R' : ' '), | |
(pub->hand[0].know[i] & (1 << (3+KNOW_COL_OFFSET)) ? 'W' : ' '), | |
(pub->hand[0].know[i] & (1 << (4+KNOW_COL_OFFSET)) ? 'Y' : ' '), | |
(pub->hand[0].know[i] & (1 << (5+KNOW_COL_OFFSET)) ? '*' : ' ') | |
); | |
} | |
fprintf(fp, "\n"); | |
fprintf(fp, "Other hands:\n"); | |
for(i = 1; i < pub->game.nplayers; ++i){ | |
fprintf(fp, " "); | |
for(j = 0; j < 3*handsize[pub->game.nplayers]; ++j){ | |
if(j == 3*handsize[pub->game.nplayers]/2){ | |
fprintf(fp, "%d", i); | |
}else{ | |
fputc('=', fp); | |
} | |
} | |
} | |
fprintf(fp, "\n"); | |
for(i = 1; i < pub->game.nplayers; ++i){ | |
fprintf(fp, " "); | |
for(j = 0; j < pub->hand[i].ncards; ++j){ | |
fprintf(fp, " "); | |
fprintcard(fp, pub->hand[i].card[j]); | |
} | |
for( ; j < handsize[pub->game.nplayers]; ++j){ | |
fprintf(fp, " "); | |
} | |
} | |
fprintf(fp, "\n"); | |
} | |
#define INFO_MASK 0xFF | |
#define INFO_WHO_MASK 0x07 | |
#define INFO_WHO_OFFSET 0 | |
#define INFO_TYPE_MASK 0x08 | |
#define INFO_TYPE_OFFSET 3 | |
#define INFO_TYPE_NUMBER 0 | |
#define INFO_TYPE_COLOR 1 | |
#define INFO_IDX_MASK 0xF0 | |
#define INFO_IDX_OFFSET 4 | |
/* default callbacks for the hanabi_player */ | |
int cb_draw_default(const struct hanabi_public *pub){ | |
return pub->hand[0].ncards; | |
} | |
int getrange(int lo, int hi){ // ask user for a number between lo and hi, inclusive. keep going until valid | |
int response = hi+1; | |
while(!(lo <= response && response <= hi)){ | |
scanf("%d", &response); | |
} | |
return response; | |
} | |
int cb_turn_interactive(const struct hanabi_public *pub, int *action){ | |
int acttype = -1; | |
int who = -1; | |
int type = -1; | |
int idx = -1; | |
fprintf(stdout, "############################################################\n"); | |
fprintpub(stdout, pub); | |
fprintf(stdout, "Action (0=discard,1=play,2=info): "); | |
acttype = getrange(0,2); | |
if(2 == acttype){ | |
fprintf(stdout, "Tell who? "); | |
who = getrange(1,pub->game.nplayers-1); | |
fprintf(stdout, "Number (0) or color (1)? "); | |
type = getrange(0,1); | |
fprintf(stdout, "What (1-5 number, 0=B,1=G,2=R,3=W,4=Y,5=*)? "); | |
if(0 == type){ | |
idx = getrange(1,5); | |
}else{ | |
idx = getrange(0,5); | |
} | |
*action = ((idx << 4) | (type << 3) | who); | |
}else{ | |
fprintf(stdout, "Which? "); | |
*action = getrange(0,pub->hand[0].ncards-1); | |
} | |
} | |
int cb_info_default(struct hanabi_hand *hand, int type, int idx, const int which[5]){ | |
return 0; | |
} | |
struct hanabi_player{ | |
// called when a card must be played | |
int (*draw_callback)(const struct hanabi_public *pub); | |
// called when an action must be taken: | |
// return 0: discard, action is the index of the card to discard | |
// 1: play, action is the index of the card to play | |
// 2: info, action is the information to say | |
// low 3 bits: player index offset to say (0 is the active player, 1 is the next, etc. 0 is invalid) | |
// next bit: 0=number, 1=color | |
// next 4 bits: the number or color index to say | |
int (*turn_callback)(const struct hanabi_public *pub, int *action); | |
// called when the player is told information | |
int (*info_callback)(struct hanabi_hand *hand, int type, int idx, const int which[5]); | |
}; | |
static int rand_int(int n){ | |
int limit = RAND_MAX - RAND_MAX % n; | |
int rnd; | |
do{ rnd = rand(); }while(rnd >= limit); | |
return rnd % n; | |
} | |
void shuffle(int n, int *array){ | |
int i; | |
for(i = n; i-- > 0; ){ | |
int j = rand_int(i + 1); | |
int tmp = array[j]; | |
array[j] = array[i]; | |
array[i] = tmp; | |
} | |
} | |
void hanabi_game_init(int nplayers, struct hanabi_game *game, struct hanabi_hand *hand){ | |
static const int numcount[6] = {-1,3,2,2,2,1}; | |
int color, number, count, i; | |
int *p; | |
// initialize the deck | |
memset(game, 0, sizeof(struct hanabi_game)); | |
game->nplayers = nplayers; | |
game->active_player = rand() % nplayers; | |
game->ninfo = 8; | |
game->nbomb = 0; | |
game->ndeck = 60; | |
p = game->deck; | |
for(color = 0; color < 6; ++color){ | |
for(number = 1; number <= 5; ++number){ | |
for(count = 0; count < numcount[number]; ++count){ | |
*p = cn2i(color,number); | |
++p; | |
} | |
} | |
} | |
// initialize the hands | |
for(i = 0; i < game->nplayers; ++i){ | |
hand[i].ncards = 0; | |
} | |
// shuffle | |
shuffle(game->ndeck, game->deck); | |
/* | |
for(i = 0; i < game->ndeck; ++i){ | |
printf(" %d", game->deck[i]); | |
} printf("\n"); | |
*/ | |
// deal | |
for(count = 0; count < handsize[game->nplayers]; ++count){ | |
for(i = 0; i < game->nplayers; ++i){ | |
hand[i].ncards++; | |
hand[i].card[count] = game->deck[DECK_SIZE - game->ndeck]; | |
game->ndeck--; | |
hand[i].know[count] = KNOW_MASK; | |
} | |
} | |
} | |
// construct the public information from the current game state | |
// currently just copies everything | |
void game_make_pub(const struct hanabi_game *game, const struct hanabi_hand *hand, struct hanabi_public *pub){ | |
int i; | |
memcpy(&pub->game, game, sizeof(struct hanabi_game)); | |
for(i = 0; i < game->nplayers; ++i){ | |
int w = (game->active_player + i) % game->nplayers; | |
memcpy(&pub->hand[i], &hand[w], sizeof(struct hanabi_hand)); | |
} | |
} | |
// advance the game by one turn | |
void hanabi_game_turn(struct hanabi_game *game, struct hanabi_hand *hand, struct hanabi_player *player){ | |
int i; | |
int action, actiontype; | |
struct hanabi_public pub; | |
// generate the public information | |
game_make_pub(game, hand, &pub); | |
// Ask player to take action | |
actiontype = player[game->active_player].turn_callback(&pub, &action); | |
if(0 == actiontype){ // discard | |
assert(0 <= action && action < hand[game->active_player].ncards); | |
game->discard[hand[game->active_player].card[action]]++; | |
for(i = action; i+1 < hand[game->active_player].ncards; ++i){ | |
hand[game->active_player].card[i] = hand[game->active_player].card[i+1]; | |
hand[game->active_player].know[i] = hand[game->active_player].know[i+1]; | |
} | |
hand[game->active_player].ncards--; | |
}else if(1 == actiontype){ // play | |
int card, cardnum, cardcol; | |
assert(0 <= action && action < hand[game->active_player].ncards); | |
card = hand[game->active_player].card[action]; | |
i2cn(card, &cardcol, &cardnum); | |
if(game->pile[cardcol]+1 == cardnum){ | |
game->pile[cardcol]++; | |
}else{ | |
game->discard[card]++; | |
game->nbomb++; | |
} | |
}else{ // information | |
int who, type, idx; | |
int which[5] = {0,0,0,0,0}; | |
who = (action & INFO_WHO_MASK ) >> INFO_WHO_OFFSET; | |
type = (action & INFO_TYPE_MASK) >> INFO_TYPE_OFFSET; | |
idx = (action & INFO_IDX_MASK ) >> INFO_IDX_OFFSET; | |
assert(0 < who && who < game->nplayers); | |
assert( | |
(type == INFO_TYPE_NUMBER && 1 <= idx && idx <= 5) || | |
(type == INFO_TYPE_COLOR && 0 <= idx && idx < 6) | |
); | |
// need to appropriately set the knowledge bits | |
who = (who+game->active_player) % game->nplayers; | |
// note: not enforcing the cant-say-no-cards-matching rule | |
for(i = 0; i < hand[game->active_player].ncards; ++i){ | |
int cardnum, cardcol; | |
i2cn(hand[who].card[i], &cardcol, &cardnum); | |
if(INFO_TYPE_NUMBER == type){ | |
const int numbit = ((1 << (cardnum-1)) << KNOW_NUM_OFFSET); | |
if(cardnum == idx){ | |
hand[who].know[i] &= ((KNOW_MASK & (~KNOW_NUM_MASK)) | numbit); | |
which[i] = 1; | |
}else{ | |
hand[who].know[i] &= (~numbit); | |
} | |
}else{ | |
const int colbit = ((1 << cardcol) << KNOW_COL_OFFSET); | |
if(cardcol == RAINBOW_COLOR_IDX || cardcol == idx){ // The card matches | |
hand[who].know[i] &= ((KNOW_MASK & (~KNOW_COL_MASK)) | KNOW_RAINBOW_BIT | colbit); | |
which[i] = 1; | |
}else{ | |
hand[who].know[i] &= (~(KNOW_RAINBOW_BIT | colbit)); | |
} | |
} | |
} | |
player[who].info_callback(&hand[who], type, idx, which); | |
} | |
// Ask player to draw a card | |
if(game->ndeck > 0 && hand[game->active_player].ncards < handsize[game->nplayers]){ | |
int i; | |
struct hanabi_public pub; | |
game_make_pub(game, hand, &pub); | |
action = player[game->active_player].draw_callback(&pub); | |
assert(0 <= action && action <= hand[game->active_player].ncards); | |
for(i = hand[game->active_player].ncards; i > action; ++i){ | |
hand[game->active_player].card[i] = hand[game->active_player].card[i-1]; | |
hand[game->active_player].know[i] = hand[game->active_player].know[i-1]; | |
} | |
hand[game->active_player].card[action] = game->deck[DECK_SIZE-game->ndeck]; | |
game->ndeck--; | |
hand[game->active_player].know[action] = KNOW_MASK; | |
hand[game->active_player].ncards++; | |
} | |
// Advance turn | |
game->active_player = (game->active_player+1)%game->nplayers; | |
} | |
int main(int argc, char *argv[]){ | |
int i; | |
int nplayers = 3; | |
int turn, nturns; | |
struct hanabi_game game; | |
struct hanabi_hand hand[5]; | |
struct hanabi_player player[5]; | |
if(argc > 1){ | |
nplayers = atoi(argv[1]); | |
if(nplayers < 3){ nplayers = 3; } | |
if(nplayers > 5){ nplayers = 5; } | |
} | |
hanabi_game_init(nplayers, &game, &hand[0]); | |
for(i = 0; i < nplayers; ++i){ | |
player[i].draw_callback = &cb_draw_default; | |
player[i].turn_callback = &cb_turn_interactive; | |
player[i].info_callback = &cb_info_default; | |
} | |
// determine how many turns should occur | |
nturns = game.ndeck + game.nplayers; | |
for(turn = 0; turn < nturns; ++turn){ | |
hanabi_game_turn(&game, &hand[0], &player[0]); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment