The game "Rock Paper Scissors" can be specified in terms of states.
Specification
The game is played by two players, playerA
and playerB
. Each player selects from among a set of three options {null, rock, paper, scissors}
. null
is used to represent the state before a player has chosen. Using an ordered pair (playerA_choice, playerB_choice)
creates the possible game states:
(null, rock)
(null, paper)
(null, scissors)
(rock, null)
(rock, rock)
(rock, paper)
(rock, scissors)
(paper, null)
(paper, rock)
(paper, paper)
(paper, scissors)
(scissors, null)
(scissors, rock)
(scissors, paper)
(scissors, scissors)
There are three final states and three transition functions to them:
playerA_wins = (rock, scissors) | (paper, rock) | (scissors, paper)
playerB_wins = (rock, paper) | (paper, scissors) | (scissors, rock)
draw = (rock, rock) | (paper, paper) | (scissors, scissors)
The start state is:
(null, null)
The states:
(null, rock)
(null, paper)
(null, scissors)
(rock, null)
(paper, null)
(scissors, null)
are blocking and require further input from one of the players.
Implementing The Specification
This is a sketch of the game:
# Some Useful Names
null = "null"
rock = "rock"
paper = "paper"
scissors = "scissors"
# A Thesaurus (implemented as a dictionary)
synonyms = {"rock": rock,
"paper": paper,
"scissors": scissors,
"stone": rock,
"vellum": paper,
"shears": scissors}
# Final States
game_is_draw = "Game is a Draw"
playerA_wins = "Player A Wins"
playerB_wins = "Player B Wins"
# Initial State
both_players_must_choose = "Both Players Must Choose"
# Transition States
playerA_must_choose = "Player A Must Choose"
playerB_must_choose = "Player B Must Choose"
# Transition Table (implemented as a dictionary)
transitions = {(null, null): both_players_must_choose,
(null, rock): playerA_must_choose,
(null, paper): playerA_must_choose,
(null, scissors): playerA_must_choose,
(rock, null): playerB_must_choose,
(rock, rock): game_is_draw,
(rock, paper): playerB_wins,
(rock, scissors): playerA_wins,
(paper, null): playerB_must_choose,
(paper, rock): playerA_wins,
(paper, paper): game_is_draw,
(paper, scissors): playerB_wins,
(scissors, null): playerB_must_choose,
(scissors, rock): playerB_wins,
(scissors, paper): playerA_wins,
(scissors, scissors): game_is_draw}
# Simulate Initialization
playerA_choice = null
playerB_choice = null
# Simulate Players Choosing
playerA_choice = synonyms["stone"]
playerB_choice = synonyms["shears"]
# Main Logic
state = (playerA_choice, playerB_choice)
print(outcomes[state])
Details to handle input output should be at a higher layer of abstraction. It shouldn't matter to the game engine if the game is between a human and a computer, two humans, or two computers. It shouldn't matter if it is being played using a laptop or over the internet.
Data Structures
A good rule of thumb is to replace complex logic with a data structure, and
if choice == 'r' or choice == 'R' or choice == 'Rock' or choice == 'rock' or choice == '1':
is the sort of code that is hard to understand and hard to maintain. Perhaps to the point where it is better to forgo the user friendly approach? Nay! A thesaurus is a good place to look for synonyms. Although, Python lacks Thesauri, a dictionary will probably do. Now the game can be sold at Ye Local Renaissance Faire as Stone, Vellum, Shears!
Indeed, a dictionary is a good way to map each possible game state to the next state. The code does in lieu of directly implementing the logic the specification uses to describe the final states.
The reason is maintainability. The mobile version of the game will offer an upgrade to Rock, Paper, Scissors, Spock, Lizard currently in development. Once we get around to updating the dictionaries synonyms
and transitions
the game will be done, profits will role in, and we will never have to work again unless sleeping on a stack of money counts as work.
But Really, Why All the Ceremony?
One of the real values that comes from using dictionaries is that a dictionary can be used to dispatch functions.
# Abbreviated Rock Paper Scissors
# Some Useful Names
rock = "rock"
paper = "paper"
def playerA_wins():
print("Player A Wins")
def playerB_wins():
print("Player B Wins")
transitions = {(paper, rock): playerA_wins,
(rock, paper): playerB_wins}
transitions[(paper, rock)]()
transitions[(rock, paper)]()
Getting user input can be dispatched similarly by the dictionary. However, the initial (null, null)
state, threading might be justified so that playerA_choice()
doesn't block playerB_choice()
or vice versa...that's IO for 'ya.
Recommendations
- Better variable names. Write about rocks and paper and scissors:
rock = 1
makes the code more readable. Readability particularly helps the person writing the code.
- Make the code more modular. Put all the initialization together. Put all the user interface someplace else. Separate out the string matching. Only put high level abstractions in the main loop.
- Consider specifying the problem before writing code for a solution. At the core Rock, Paper, Scissors does not deal with synonyms. Writing a specification makes it clear that synonyms are a feature and encourages keeping their logic outside the main loop. This makes the code more readable.