Skip to content

Instantly share code, notes, and snippets.

@dschuetz
Created March 28, 2021 19:50
Show Gist options
  • Save dschuetz/4025f7aa038201abeadef4c41d1027a9 to your computer and use it in GitHub Desktop.
Save dschuetz/4025f7aa038201abeadef4c41d1027a9 to your computer and use it in GitHub Desktop.
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