Created
March 28, 2021 19:50
-
-
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
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
#!/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