Last active
March 30, 2023 18:08
-
-
Save CtrlAltCuteness/6007d07a05035459a7d58a107b22c4fe to your computer and use it in GitHub Desktop.
A stupidly intelligent-ish Rock Paper Scissors program
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/python3 | |
# vim: fileencoding=utf8:filetype=python:nu:expandtab | |
# Updated for 3.10 / 3.11 | |
__all__ = ['getUserChocie', 'playForever'] | |
from typing import Any, Literal, Optional | |
from collections.abc import Callable, Iterable | |
from random import randrange | |
# A utility function to prompt the user for a choice. | |
# Type hinting needs improvements to properly work. | |
def getUserChoice( | |
choices: dict[str, Any], | |
prompt: str = '', | |
/, *, | |
mapfunc: Optional[Callable[[str], str | None]] = None, | |
clear: Literal['always', 'once', 'never'] = 'never', | |
cleartext: str = '\033[H\033[2J', | |
) -> Any: | |
if clear == 'once': | |
print(cleartext, end='', flush=True) | |
elif clear == 'always': | |
prompt = cleartext + prompt | |
inp: Optional[str] = None | |
while True: | |
inp: str = input(prompt) | |
if mapfunc is not None: | |
inp = mapfunc(inp) | |
if inp is None: | |
continue | |
if inp in choices: | |
return choices[inp] | |
# Helper function for my main loop below. | |
def toLowerAndStrip(text: str, /) -> str: | |
return text.strip().lower() | |
def multiMapDict(entries: Iterable, /) -> dict: | |
result = {} | |
for value, *keys in entries: | |
for key in keys: | |
result[key] = value | |
return result | |
# Main loop. | |
# tuple returned: (total, wins, losses, ties) | |
def playForeverRPS() -> tuple[int, int, int, int]: | |
# The constants for this function. | |
# For the display side as well as for | |
# the automatic picking by the CPU by | |
# checking its length. | |
CHOICE_NAMES: tuple[str, ...] = ('Rock', 'Paper', 'Scissors') | |
# This is the mapping used for when | |
# asking the player for a choice. | |
# 'None' is being used to single | |
# that the player is done playing. | |
# The others must be a valid index | |
# to CHOICE_NAMES above. | |
CHOICE_MAPPING: dict[str, int | None] = multiMapDict(( | |
(0, 'r', 'rock'), | |
(1, 'p', 'paper'), | |
(2, 's', 'scissors', 'scissor'), | |
(None, 'q', 'quit') | |
)) | |
PROMPT: str = '''\ | |
Win/Loss/Draw: {win}/{loss}/{draw} | |
Total: {total} | |
{you_msg}{you_pick} | |
{cpu_msg}{cpu_pick} | |
{out_msg} | |
Pick your choice: | |
[R]ock, [P]aper, [S]cissors | |
[Q]uit | |
> ''' | |
# The mutable values for this function. | |
# These two are plopped into the | |
# 'PROMPT' template string while | |
# 'stats' is also reused when | |
# returning from this function. | |
stats: dict[str, int] = { | |
'win': 0, 'loss': 0, | |
'draw': 0, 'total': 0 | |
} | |
messages: dict[str, str] = { | |
'you_msg': '', 'you_pick': '', | |
'cpu_msg': '', 'cpu_pick': '', | |
'out_msg': '' | |
} | |
# Assigning them here for type hints | |
chosen: Optional[int] = None | |
bot: int = 0 | |
while True: | |
try: | |
chosen = getUserChoice( | |
CHOICE_MAPPING, | |
PROMPT.format(**stats, **messages), | |
mapfunc=toLowerAndStrip, # be a bit forgiving | |
clear='always' # works best in *nix-like or PowerShell | |
) | |
#except KeyboardInterrupt as e: | |
# raise e | |
except EOFError: | |
# assume the use wanted to quit | |
chosen = None | |
if chosen is None: | |
return stats['total'], stats['win'], stats['loss'], stats['draw'] | |
# pick the bot's choice | |
bot = randrange(0, len(CHOICE_NAMES)) | |
# assign to the strings for who picked what | |
messages['you_pick'] = CHOICE_NAMES[chosen] | |
messages['cpu_pick'] = CHOICE_NAMES[bot] | |
# On first time, add the strings in. | |
# Also, increase the total played by 1. | |
if not stats['total']: | |
messages['you_msg'] = 'You picked: ' | |
messages['cpu_msg'] = 'CPU picked: ' | |
stats['total'] += 1 | |
# Assuming it was not altered ... | |
# 0 = rock, 1 = paper, 2 = scissors | |
match (chosen - bot + 3) % 3: | |
case 0: # Tie | |
messages['out_msg'] = ' == Tie Game ==' | |
stats['draw'] += 1 | |
case 1: # Win | |
messages['out_msg'] = '== You Win! ==' | |
stats['win'] += 1 | |
case 2: # Loss | |
messages['out_msg'] = '== You Lose ==' | |
stats['loss'] += 1 | |
def main() -> int: | |
from sys import argv | |
outid = None | |
# If any argument is specified, it is assumed to | |
# be the file descriptor to use for the results. | |
if len(argv) > 1: | |
try: | |
outid = int(argv[1]) | |
except ValueError: | |
return 1 # Invalid input | |
if outid is None: | |
# No argument specified, just play and | |
# ignore the results. | |
playForeverRPS() | |
return 0 | |
# It will error here if using STDIN or any | |
# file descriptor that is not available. | |
# Depending on the input, redirection or | |
# pipe, it may even stack another error. | |
try: | |
with open(outid, 'w', closefd=False) as f: | |
print(*playForeverRPS(), sep=' ', file=f) | |
except KeyboardInterrupt: | |
return 2 | |
except OSError: | |
return 1 # Most likely the error stated above | |
return 0 | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment