├── .gitignore ├── LICENSE ├── Manifest.in ├── README.md ├── mastermind ├── __init__.py ├── __main__.py ├── mastermind.py ├── params.py └── version ├── requirements.txt ├── setup.py └── tests └── test_import.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv* 2 | *.pyc 3 | .vscode 4 | __pycache__ 5 | build 6 | dist 7 | open_mastermind.egg-info 8 | .tox 9 | INFO.txt 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 @philshem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Manifest.in: -------------------------------------------------------------------------------- 1 | include requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-mastermind 2 | Terminal-based python of classic board game [Mastermind®](https://en.wikipedia.org/wiki/Mastermind_(board_game)) 3 | 4 | 5 | ![Mastermind board game](https://upload.wikimedia.org/wikipedia/commons/thumb/2/2d/Mastermind.jpg/137px-Mastermind.jpg) 6 | 7 | ![screenshot of gameplay](https://gist.githubusercontent.com/philshem/71507d4e8ecfabad252fbdf4d9f8bdd2/raw/e00c621f403520d3268f2a9ece176fb2f05f2185/mastermind.png) 8 | 9 | ## to get the game 10 | 11 | Install with `pip` from the command line: 12 | 13 | pip install open-mastermind 14 | 15 | And run with this command: 16 | 17 | mastermind 18 | 19 | Although written in Python3.7, it's compatible back to Python2.7. 20 | 21 | ## example game play 22 | 23 | Each puzzle contains 4 boxes. Each turn you choose from 6 colors. 24 | 25 | Can you guess the puzzle before your turns run out? 26 | 27 | Color choices: r g y b m w 28 | 29 | Example turn: rybg 30 | 31 | Response: 32 | 33 | ◍ : correct color in correct position 34 | 35 | ○ : correct color in incorrect position 36 | 37 | _ : incorrect color 38 | 39 | The order of the response tiles is sorted and does not necessarily match the position of the colored tiles. 40 | 41 | Type !h during gameplay to read these instructions. 42 | 43 | Type !q during gameplay to quit and show the solution. 44 | 45 | ## 2-player mode 46 | 47 | Solutions can be hashed and shared with other players. To generate a code for the solution `red-green-yellow-blue` (rgyb): 48 | 49 | mastermind rgyb 50 | 51 | > Your code to play rgyb is 20419 52 | 53 | Then share the code `20419` with the other player, who plays the desired game like this: 54 | 55 | mastermind 20419 56 | 57 | It's _serverless_ and also on the honor system. Codes are generated on the fly based on the [16-bit CRC value](https://docs.python.org/2/library/binascii.html#binascii.crc_hqx). 58 | 59 | # to get the code 60 | 61 | Clone the repository, then install the requirements (currently only the non-standard python library [`colorama`](https://pypi.org/project/colorama/), which prints a colorful terminal.) 62 | 63 | git clone https://github.com/philshem/open-mastermind.git 64 | cd open-mastermind 65 | pip install -r requirements.txt 66 | 67 | And to play from your downloaded code: 68 | 69 | python mastermind.py 70 | 71 | 72 | ## change the game parameters 73 | 74 | Edit the file `params.py` to: 75 | 76 | + more pieces to guess 77 | 78 | + more/fewer turns per game 79 | 80 | + new colors 81 | 82 | + different emoji playing pieces 83 | -------------------------------------------------------------------------------- /mastermind/__init__.py: -------------------------------------------------------------------------------- 1 | from .mastermind import * -------------------------------------------------------------------------------- /mastermind/__main__.py: -------------------------------------------------------------------------------- 1 | import mastermind 2 | import params 3 | 4 | mastermind.main() -------------------------------------------------------------------------------- /mastermind/mastermind.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | ''' play mastermind ''' 5 | 6 | # python2 compatability 7 | from __future__ import print_function 8 | try: 9 | import __builtin__; input = getattr(__builtin__, 'raw_input', input) 10 | except ImportError: 11 | pass 12 | 13 | # internal parameters 14 | from . import params 15 | #import params 16 | 17 | import os, sys 18 | import random 19 | import itertools 20 | from collections import Counter, defaultdict 21 | import binascii 22 | from colorama import init, Style, Fore 23 | 24 | def generate_board(): 25 | 26 | # generates a random board based on params.py 27 | return [random.choice(list(params.color_dict.keys())) for x in range(params.count_boxes)] 28 | 29 | def make_guess(msg, master): 30 | 31 | guess = input(msg).lower().strip() 32 | 33 | if guess.startswith('!h'): 34 | print_instructions() 35 | return 36 | elif guess.startswith('!q'): 37 | print ('You chose to quit. The solution is:') 38 | print_colors(master, params.guess_peg, True) 39 | exit(0) 40 | 41 | elif len(guess) != params.count_boxes: 42 | print('Invalid guess. Must be',str(params.count_boxes),'colors.','\n') 43 | return None 44 | elif not all([x in params.color_dict.keys() for x in list(guess)]): 45 | print('Invalid guess. Must include only letters: '+print_color_choices()+'\n') 46 | return None 47 | else: 48 | return guess 49 | 50 | def print_colors(inp, guess_char, tf_master): 51 | 52 | # print colored characters, suppress new line 53 | 54 | print(' '.join([params.color_dict.get(x) + guess_char for x in inp]),end='\t') 55 | if tf_master: 56 | print(Style.RESET_ALL,end='\n') 57 | else: 58 | print(Style.RESET_ALL,end='\t') 59 | return 60 | 61 | def print_instructions(): 62 | print('Puzzle contains '+str(params.count_boxes)+' boxes. Each turn you choose from '+str(params.count_colors)+' colors.') 63 | print('Color choices: '+print_color_choices()) 64 | print('Example turn: rybg') 65 | print('Response:') 66 | print(params.answer_dict.get('1')+' : correct color in correct position') 67 | print(params.answer_dict.get('2')+' : correct color in incorrect position') 68 | print(params.answer_dict.get('9')+' : incorrect color') 69 | print() 70 | print('The order of the response tiles does not necessarily match the colored characters.') 71 | print('Type !h to read these instructions again.') 72 | print('Type !q to quit and show solution.') 73 | print() 74 | 75 | def print_color_choices(): 76 | 77 | return ' '.join([v+k+Style.RESET_ALL for k,v in params.color_dict.items()]) 78 | 79 | 80 | def print_results(inp): 81 | 82 | grid = [Fore.WHITE + Style.BRIGHT + params.answer_dict.get(x) for x in inp] 83 | print(' '.join(grid)) 84 | print(Style.RESET_ALL) 85 | return 86 | 87 | def get_results(guess, master): 88 | 89 | response = [] 90 | 91 | # much simpler logic 92 | # https://stackoverflow.com/a/45798078/2327328 93 | 94 | correct_colors = sum((Counter(guess) & Counter(master)).values()) 95 | correct_locations = sum(g == m for g, m in zip(guess, master)) 96 | 97 | correct_results = max(0,correct_colors - correct_locations) 98 | incorrect_results = max(0,params.count_boxes - correct_colors) 99 | 100 | result = '1' * correct_locations \ 101 | + '2' * correct_results \ 102 | + '9' * incorrect_results 103 | 104 | if params.debug: print(result) 105 | 106 | # something went wrong 107 | if len(result) != params.count_boxes: 108 | print('🚀 Houston. We have a problem.') 109 | exit(0) 110 | 111 | # sort response array - comment out for debugging 112 | #response = list(sorted(response)) 113 | 114 | return result 115 | 116 | def validate_puzzle(t): 117 | 118 | if len(t) != params.count_boxes: 119 | print ('Incorrect number of boxes. Needs to be {} boxes.'.format(params.count_boxes)) 120 | exit(0) 121 | elif any([x for x in t if x not in params.color_dict.keys()]): 122 | print ('Incorrect color selected. Choose from {}.'.format(print_color_choices())) 123 | exit(0) 124 | else: 125 | #print(list(t)) 126 | return True 127 | 128 | def generate_code(): 129 | 130 | xx = defaultdict(int) 131 | 132 | all_combos = [p for p in itertools.product(params.color_dict.keys(), repeat=params.count_boxes)] 133 | 134 | for aa in all_combos: 135 | t = ''.join(aa) 136 | z = binascii.crc_hqx(t.encode('ascii'), 0) 137 | #print(t,str(z).zfill(5)) 138 | xx[str(z).zfill(5)] = t 139 | 140 | return xx 141 | 142 | def main(): 143 | 144 | # colorama initialization 145 | init() 146 | 147 | # generate code table (dict) 148 | dd = generate_code() 149 | 150 | # read system args, if any 151 | master = None 152 | # play random (normal) game if no input params are given 153 | if len(sys.argv) < 2: 154 | # generate solution 155 | master = generate_board() 156 | 157 | # check that not too many input params are given 158 | elif len(sys.argv) > 2: 159 | print('Incorrect number of system arguments.') 160 | exit(0) 161 | 162 | # check if puzzle is provided to generate code (no game is played in this case) 163 | elif sys.argv[1] in dd: 164 | #print(dd.get(sys.argv[1])) 165 | master = list(dd.get(sys.argv[1])) 166 | 167 | # generate puzzle based on code 168 | elif validate_puzzle(sys.argv[1]): 169 | rd = dict(map(reversed, dd.items())) 170 | if sys.argv[1] in rd: 171 | print('Your code to play {} is {}'.format(sys.argv[1],rd.get(sys.argv[1]))) 172 | exit(0) 173 | 174 | # if you made it this far, there is no game to play 175 | else: 176 | #print('Invalid input parameters.') 177 | exit(0) 178 | 179 | # clear terminal 180 | os.system('clear') 181 | 182 | #cheat or debug mode 183 | if params.debug: 184 | print_colors(master, params.guess_peg, True) 185 | 186 | print_instructions() 187 | 188 | guess_list = [] 189 | for i in range(params.count_turns): 190 | guess = None 191 | while not guess: 192 | 193 | # create message based on turn number 194 | if i + 1 < params.count_turns: 195 | msg = 'Turn ' 196 | else: 197 | msg = 'Last turn! ' 198 | msg += str(i+1)+' of '+str(params.count_turns)+'. Your guess: ' 199 | 200 | # request guess from user 201 | guess = make_guess(msg, master) 202 | 203 | print_colors(guess, params.guess_peg, False) 204 | guess_list.append(guess) 205 | 206 | # check guess - if you made it this far, 207 | # the guess is valid and there are still more turns 208 | result = get_results(guess, master) 209 | 210 | print_results(result) 211 | 212 | if result == '1' * params.count_boxes: 213 | print('🎉 Congrats mastermind! You found the solution:') 214 | print_colors(master,params.guess_peg,True) 215 | print() 216 | exit(0) 217 | 218 | # game over... too many turns used 219 | if len(guess_list) >= params.count_turns: 220 | print ('Too many turns! The solution is:') 221 | print_colors(master,params.guess_peg,True) 222 | exit(0) 223 | 224 | if __name__ == "__main__": 225 | 226 | main() 227 | -------------------------------------------------------------------------------- /mastermind/params.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | ''' params for mastermind ''' 5 | 6 | from colorama import Fore, Style 7 | 8 | # how many turns to allow user to guess 9 | count_turns = 10 10 | 11 | # what unicode character to show guesses 12 | # https://unicode-search.net/unicode-namesearch.pl?term=CIRCLE 13 | guess_peg = '⬤' 14 | 15 | # how many colors to guess (default = 6) 16 | count_colors = 6 17 | color_dict = { 18 | 'r' : Fore.RED + Style.BRIGHT, 19 | 'g' : Fore.GREEN + Style.BRIGHT, 20 | 'y' : Fore.YELLOW, 21 | 'b' : Fore.BLUE, 22 | 'm': Fore.MAGENTA, 23 | 'w' : Fore.WHITE, 24 | } 25 | 26 | # how many places to guess (default = 4) 27 | count_boxes = 4 28 | 29 | answer_dict = {'1' : '◍', '2' : '○', '9' : '_'} 30 | 31 | # color options 32 | # Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. 33 | # Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. 34 | # Style: DIM, NORMAL, BRIGHT, RESET_ALL 35 | 36 | 37 | # cheat mode, prints master board before gameplay 38 | debug = False -------------------------------------------------------------------------------- /mastermind/version: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | with open(path.join(here, 'requirements.txt')) as f: 7 | reqs = f.read().split() 8 | 9 | with open(path.join(here, 'README.md')) as f: 10 | readme = f.read() 11 | 12 | with open(path.join(here, 'mastermind', 'version')) as f: 13 | version = f.read().strip() 14 | 15 | setup( 16 | name='open_mastermind', 17 | version=version, 18 | description='A terminal-based code-breaking game Mastermind', 19 | long_description=readme, 20 | long_description_content_type='text/markdown', 21 | url='https://github.com/philshem/open-mastermind', 22 | author='Philip Shemella', 23 | author_email='philshem@pm.me', 24 | classifiers=[ 25 | 'Environment :: Console :: Curses', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python :: 3', 28 | 'Topic :: Games/Entertainment :: Puzzle Games', 29 | ], 30 | packages=find_packages(), 31 | python_requires='>=2.7', 32 | install_requires=reqs, 33 | package_data={ 34 | 'open_mastermind': ['version'] 35 | }, 36 | entry_points={ 37 | 'console_scripts': [ 38 | 'mastermind=mastermind:main', 39 | ], 40 | }, 41 | keywords='puz mastermind code-breaking puzzle game' 42 | ) 43 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | 6 | import os 7 | import sys 8 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 9 | 10 | import mastermind --------------------------------------------------------------------------------