My rational for this program, was to learn more of Python's standard library, I didn't actually know cmd
was even in it until I started this program. It's also the first community challenge, and I have some catching up to do.
This is an overly engineered version of the famous Rock Paper Scissors game. By default it's the exact same, but allows for people to define their own weapons, such as Rock Paper Scissors Lizard Spock, or the less liked Rock Paper Scissors Chainsaw. To do this I used configparser
for an INI like input format. The sections are the winners, where the keys that follow are the losers. I also allow keys to be passed values which are the winning action, defaulting to 'beats'. Here is an example of a configuration file used for Rock Paper Scissors Lizard Spock (saved as rpsls
, and used in the example):
[rock]
scissors: crushes
lizard: crushes
[paper]
rock: covers
spock: disproves
[scissors]
paper: cuts
lizard: decapitates
[lizard]
paper: eats
spock: poisons
[spock]
rock: vaporizes
scissors: smashes
To simplify the usage of this program I made some simple directed Graph and Node classes that inherit from dict
. Which I think follow SOLID.
I also used cmd
to create a simple command line interface. It didn't need to be anything fancy, and so it seemed like the best, and simplest, solution.
This also only works in Python 3.6, as I used f-strings.
import cmd
import configparser
import itertools
import random
class RPSException(Exception):
pass
class DirectedGraph(dict):
class Node(dict):
__slots__ = ['name', '_graph']
def __init__(self, graph, name):
self.name = name
self._graph = graph
def add_edge(self, node, edge):
self[node.name] = edge
def adjacent(self, x, y):
return x in self[y] or y in self[x]
def add_node(self, *args, **kwargs):
node = self.Node(self, *args, **kwargs)
self[node.name] = node
def add_edge(self, node_x, node_y, edge):
self[node_x].add_edge(node_y, edge)
def missing_edges(self):
for keys in itertools.combinations(self.keys(), 2):
if not self.adjacent(*keys):
yield keys
class RPSGraph(DirectedGraph):
class Node(DirectedGraph.Node):
def __str__(self):
return f'{self.name}:\n '\
+ '\n '.join(f'{v[1]} {v[0].name}' for v in self.values())
def add_edge(self, node_name, action):
node = self._graph[node_name]
edge = (node, action)
super().add_edge(node, edge)
def __str__(self):
return '\n'.join(str(node) for node in self.values())
def parse_config(cfg):
graph = RPSGraph()
for node in cfg.sections():
graph.add_node(node)
for winner in cfg.sections():
for looser, action in cfg[winner].items():
graph.add_edge(winner, looser, action or 'beats')
missing = list(graph.missing_edges())
if missing:
raise RPSException(f"Invalid configuration file. You're missing vertices between: {missing}")
return graph
class RPS(cmd.Cmd):
intro = "Welcome to Rock, Paper, Whatever. Type help or ? to list commands.\n"
prompt = '> '
cfg = configparser.ConfigParser(allow_no_value=True)
cfg.read_string('''\
[rock]
scissors: crushes
[paper]
rock: covers
[scissors]
paper: cuts
''')
graph = parse_config(cfg)
del cfg
def do_load_file(self, filename):
'''Load a custom variation of rock paper scissors from a file.'''
cfg = configparser.ConfigParser(allow_no_value=True)
cfg.read(filename)
self.graph = parse_config(cfg)
@staticmethod
def read_lines():
try:
while True:
yield input()
except KeyboardInterrupt:
pass
def do_load_stdin(self, _):
'''Load a custom variation of rock paper scissors from the command line. Send a keyboard interrupt to end input.'''
data = '\n'.join(self.read_lines())
cfg = configparser.ConfigParser(allow_no_value=True)
cfg.read_string(data)
self.graph = parse_config(cfg)
def do_play(self, weapon):
'''Play a weapon. E.g. play rock'''
graph = self.graph
if weapon not in graph:
print('Invalid weapon.')
return
ai = random.choice(list(graph.keys()))
if ai == weapon:
print('You draw')
elif ai in graph[weapon]:
print(f'You win. {weapon.capitalize()} {graph[weapon][ai][1]} {ai}')
else:
print(f'You loose. {ai.capitalize()} {graph[ai][weapon][1]} {weapon}')
def do_weapons(self, _):
'''List usable weapons.'''
print(', '.join(self.graph.keys()))
if __name__ == '__main__':
RPS().cmdloop()
Example usage:
Welcome to Rock, Paper, Whatever. Type help or ? to list commands.
> help
Documented commands (type help <topic>):
========================================
help load_file load_stdin play weapons
> help play
Play a weapon. E.g. play rock
> help weapons
List usable weapons.
> weapons
rock, paper, scissors
> play rock
You win. Rock crushes scissors
> load_file rpsls
> weapons
rock, paper, scissors, lizard, spock
> play rock
You loose. Spock vaporizes rock
> load_stdin
[rock]
scissors: crushes
[paper]
rock: covers
[scissors]
paper: cuts
> weapons
rock, paper, scissors
> play rock
You draw
>