Skip to content

Instantly share code, notes, and snippets.

@CtrlAltCuteness
Last active March 30, 2023 18:08
Show Gist options
  • Save CtrlAltCuteness/6007d07a05035459a7d58a107b22c4fe to your computer and use it in GitHub Desktop.
Save CtrlAltCuteness/6007d07a05035459a7d58a107b22c4fe to your computer and use it in GitHub Desktop.
A stupidly intelligent-ish Rock Paper Scissors program
#!/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