Board generator for the game Codenames - supports arbitrary sizes and up to 5 teams
#!/usr/bin/python | |
# David Schuetz | |
# March, 2021 | |
# | |
# Generates a secret Codenames board. | |
# For an explanation of just WTF this is for, see my blog post: | |
# https://darthnull.org/fun/2021/03/codenames-board-generator/ | |
# | |
############################################################################ | |
### | |
### $ python codenames.py -h | |
### usage: codenames.py [-h] [-c C] [-r R] [-t T] keyword | |
### | |
### Generate Codenames board | |
### | |
### positional arguments: | |
### keyword Any random word. Uniquely seeds the board layout. | |
### | |
### optional arguments: | |
### -h, --help show this help message and exit | |
### -c C Set width of board to C columns | |
### -r R Set height to R rows. If omittted, builds a square. | |
### -t T Number of teams (max: 5, default: 2) | |
### | |
### | |
### $ python big-codenames.py -c 8 -r 5 -t 3 WrongCowCellPaperclip | |
### | |
### Generating board of 8 x 5 (40 cards) | |
### Board seeded with the keyword: "WrongCowCellPaperclip". | |
### | |
### Green goes first. | |
### | |
### B K R T B R G G | |
### R T R T G G G R | |
### T G T G B R B B | |
### B R T G T T G T | |
### R G B B R B G B | |
### | |
### Total cards and Team Playing Order: | |
### Green: 11 | |
### Blue: 10 | |
### Red: 9 | |
### Tan: 9 | |
### | |
############################################################################ | |
from Cryptodome.Hash import SHA512 | |
import sys, argparse | |
# | |
# ANSI colors were cool, back when I wrote BBS code. In, like, 1987. | |
# | |
ANSI = {'R':'31', 'B':'34', 'G':'32', 'Y':'93', | |
'P':'35', 'T':'38;5;179', 'K':'40;37'} | |
# | |
# Let's get ridiculous and support three extra teams. | |
# | |
Teams = ['Blue', 'Red', 'Green', 'Yellow', 'Purple'] | |
############################################################################ | |
def generate_board(keyword, width, height, num_teams): | |
print("\nGenerating board of %d x %d (%d cards)" | |
% (width, height, width*height)) | |
print("Board seeded with the keyword: \"%s\".\n" % keyword) | |
hash = SHA512.new() # Basically random, but, deterministic. | |
board_size = width * height | |
card_nums = [] | |
repeat = True | |
while repeat: | |
repeat = False # let's be optimistic | |
# 1st pass: user supplied keyword | |
hash.update(keyword.encode()) # extra passes: last pass' hash | |
hash_dec = int(hash.hexdigest(), 16) # convert to Really-Big-Number(tm) | |
start_team = hash_dec % num_teams # randomly select starting team | |
# convert the decimal version of the hash digest to a new number, base N | |
# (where N is the board size). Store the result as an array (rather than | |
# figuring out a suitable alphanumeric alphabet for the "number"). | |
new_nums = [] | |
while hash_dec > 0: | |
hash_dec,rem = divmod(hash_dec, board_size) | |
new_nums.append(rem) | |
new_nums.reverse() # doesn't matter, but you know... | |
for n in new_nums: # add the digits to the list | |
card_nums.append(n) # (extending it on extra passes) | |
# shrink the list of Teams to just how many we're using this time | |
# and re-order it to account for the randomly selected first team | |
temp = Teams[0:num_teams] | |
cur_teams = (temp[start_team:] + temp[0:start_team])[0:num_teams] | |
# now we start building the board. | |
# first, figure out how many cards the first team has to guess. | |
# (see the blog post for the math) | |
max_cards = round( | |
(board_size + (num_teams*(num_teams+1))/2 - 1) / (num_teams+1) | |
) | |
board_dat = ['T'] * board_size # everything starts as Tan | |
board_dat[card_nums[0]] = 'K' # the 1st digit is the Assassin | |
idx = 1 # idx 0 = K, so start with next | |
for t in range(0, num_teams): # identify cards for each team | |
team_cards = max_cards - t | |
while (team_cards > 0) and (not repeat): | |
if board_dat[card_nums[idx]] == 'T': # still tan? | |
board_dat[card_nums[idx]] = cur_teams[t][0] # then mark it | |
team_cards = team_cards - 1 # for this team | |
idx += 1 # move to the next num regardless | |
if idx == len(card_nums): | |
print("Oops! Ran out of numbers! Extending...") | |
repeat = True | |
if not repeat: # we made it through this pass | |
counts = {} # count colors to verify math | |
print("%s goes first.\n" % cur_teams[0]) | |
for r in range(0, height): # loop down... | |
print(" ",) | |
for c in range(0, width): # ...and across | |
color = board_dat[c + r * width] # what's in this spot? | |
card_count = counts.get(color, 0) # get count for color | |
counts[color] = card_count + 1 # and add one to it | |
# finally, print out this cell using fancy terminal color tricks | |
print("\x1b[1;%sm%c\x1b[0m " % (ANSI[color], color),end='') | |
# and print a summary report of all the teams' card counts | |
# to enure we didn't mess anything up | |
# (and make it easier for the Spymasters) | |
print("\n\nTotal cards and Team Playing Order:") | |
for team in cur_teams: | |
print(" \x1b[%sm%s\x1b[0m: %d" | |
% (ANSI[team[0]], team, counts[team[0]])) | |
print(" \x1b[%sm%s\x1b[0m: %d" | |
% (ANSI['T'], 'Tan', counts['T'])) | |
else: # "not repeat" failed | |
keyword = hash.hexdigest() # we ran out of numbers, so make | |
# another pass, using the hash | |
# we generated this round as the | |
# next round's keyword | |
############################################################################ | |
def main(): | |
parser = argparse.ArgumentParser(description="Generate Codenames board") | |
parser.add_argument("keyword", | |
help="Any random word. Uniquely seeds the board layout.") | |
parser.add_argument('-c', type=int, | |
help='Set width of board to C columns') | |
parser.add_argument('-r', type=int, | |
help='Set height to R rows. If omittted, builds a square.') | |
parser.add_argument('-t', type=int, | |
help='Number of teams (max: 5, default: 2)') | |
args = parser.parse_args() | |
keyword = args.keyword | |
width = 5 # default board size and team count | |
height = 5 | |
teams = 2 | |
if (args.c !=None) and (args.r !=None): # specified both columns and rows | |
width = args.c | |
height = args.r | |
elif (args.c != None): # just gave columns == square board | |
width = args.c | |
height = args.c | |
elif (args.r != None): # gave rows but not columns - error | |
print("Must provide the columns count as well") | |
sys.exit(0) | |
if (args.t != None): # go wild! try 3, 4, or 5 teams! | |
if args.t < 2 or args.t > 5: | |
print("Team count must be between 2 and 5.") | |
sys.exit(0) | |
else: | |
teams = args.t | |
if (width * height) < 6: # 2x3 board is silly, but whatever | |
print("You have to have at least a 2x3 board!") | |
sys.exit(0) | |
generate_board(keyword, width, height, teams) | |
############################################################################ | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment