OOP and Advent Of Code Rock Paper Scissors
This year's advent of code is already over. I had started it with an energetic optimism that fueled my belief that "This year I'm gonna do it all! I am even gonna do it twice in a new language!". Obviously, like many others, I hope, I ended up solving something like the first 10 days in Python and the first 3 in Rust. I then dedicated myself solely to food coma, gaming and exploring old towns. However, in the first days I have found that some of the problems could be solved in interesting ways if approached in a more non-scripty way, so that they could be teachable examples of broader concepts of OOP, functional programming and design patterns.
Breaking the game down
What is Rock, Paper, Scissors?
If you break down rock, paper, scissors it is: a game based on moves. The relationship of these moves among themselves is such that each move is either greater or smaller in value than the others, and only equal in value to itself (to the same move).
In this definition we are obviously not taking into account the fact that the game may be divided in innings (winner is the best out of 3), or that moves have to be performed at the same time.
A game based on moves and dunder methods
Since this is a game based on moves, the most basic and primitive unit is a move. As we said, this move object will have to be grater than another move, smaller than another move and equal only to itself.
We could hardcode most of that, but if we want to keep it as generic
as possible, we can rely on the __gt__
, __lt__
and __eq__
dunder
methods to implement our comparisons between moves. The other move's
class name will be our point of reference in this comparison, and we
will store the moves that are greater than or smaller than in
attributes. This could be an abstract class as well.
class Move:
gt = ""
lt = ""
eq = ""
def __gt__(self, __o):
if __o.__class__.__name__.lower() == self.gt:
return True
return False
def __lt__(self, __o):
if __o.__class__.__name__.lower() == self.lt:
return True
return False
def __eq__(self, __o: object) -> bool:
if __o.__class__.__name__.lower() == self.eq:
return True
return False
Rock, Paper, Scissors
Given the previous premise, we can implement the moves from Rock, Paper, Scissors as such, and we can also include as attributes some of the values needed to complete the advent of code task (in the puzzle each move has a different value and a different "code" assigned).
class Rock(Move):
value = 1
code = "A"
gt = "scissors"
lt = "paper"
eq = "rock"
class Paper(Move):
value = 2
code = "B"
gt = "rock"
lt = "scissors"
eq = "paper"
class Scissors(Move):
value = 3
code = "C"
gt = "paper"
lt = "rock"
eq = "scissors"
We can already compare these moves and, basically, play the game.
my_move = Scissors()
opponent_move = Paper()
print(my_move > opponent_move)
print(my_move < opponent_move)
print(my_move == opponent_move)
Abstract Factories can be just functions
However, the puzzle asks us to derive the move from a set of letters, each move can be mapped to two letters. We could set up a whole factory class, but in Python factories can just be reduced to single functions in many cases, such as this one. So that, based on the letter passed by the puzzle's input, we generate the corresponding move:
def move_factory(move):
if move in ("A", "X"):
return Rock()
elif move in ("B", "Y"):
return Paper()
elif move in ("C", "Z"):
return Scissors()
Complete solution
You can check out the complete file and how I ended up solving the puzzle here: https://github.com/andcarnivorous/adventofcode2022/blob/master/day2/main.py
What more? Lizards and Spok
After implementing Rock, Paper, Scissors as such, the question is whether we could easily extend the game to include something like lizard and Spock? Obviously the answer is yes, the actual question is whether I will include it in this same post or go back to holiday food.