Credit: Concepts explained here are based on a book, “増補改訂版Java言語で学ぶデザインパターン入門”.
A strategy pattern makes it easy to change an algorithm to solve a problem.
Let’s run a sample program which uses two different strategies for Rock paper scissors.
- Hand: A class representing a hand, one of Rock, Paper and Scissors.
- Strategy: A class representing strategy for winning a game.
- WinningStrategy: A class representing a first concrete strategy for winning a game.
- ProbStrategy: A class representing a second concrete strategy for winning a game.
- Player: A class representing a player of a game.
Hand class has hand value attributes as an integer type that represents rock, paper, scissors. This class has a method to compare two hands to know that which hand wins a game or is stronger.
# hand.py
class Hand:
HANDVALUE_ROCK = 0
HANDVALUE_PAPER = 1
HANDVAUE_SCISSORS = 2
hand = []
name = ["Rock", "Paper", "Scissors"]
def __init__(self, handvalue):
self._handvalue = handvalue
@classmethod
def append_hands(cls):
cls.hand.append(Hand(cls.HANDVALUE_ROCK))
cls.hand.append(Hand(cls.HANDVALUE_PAPER))
cls.hand.append(Hand(cls.HANDVAUE_SCISSORS))
@classmethod
def get_hand(cls, handvalue):
cls.append_hands()
return cls.hand[handvalue]
def is_stronger_than(self, h):
return self.fight(h) == 1
def is_weaker_than(self, h):
return self.fight(h) == -1
def fight(self, h):
if (self == h):
return 0
elif (self._handvalue + 1) % 3 == h._handvalue:
return 1
else:
return -1
def to_string(self):
return self.name[self._handvalue]
Strategy Interface defines two methods. A next_hand is to return a next hand based on some strategy. A study method is to memorize a result of a game.
# strategy.py
from abc import ABC, abstractmethod
class Strategy:
@abstractmethod
def next_hand(self):
pass
@abstractmethod
def study(self, win):
pass
WinningStrategy implements Strategy class. It returns a same hand as a previous one if it won a previous game and it returns a random hand if it did not won the game.
# winning_strategy.py
from strategy import Strategy
from hand import Hand
import numpy as np
class WinningStrategy(Strategy):
def __init__(self, seed):
self._random = np.random.seed(seed)
self._won = False
self._prev_hand = None
def next_hand(self):
if not self._won:
self._prev_hand = Hand.get_hand(np.random.randint(3))
return self._prev_hand
def study(self, win):
self._won = win
ProbStrategy class implements Strategy class. This class returns a hand based on the probability of winning for a different combination of hands.
# prob_strateg.py
from strategy import Strategy
from hand import Hand
import numpy as np
class ProbStrategy(Strategy):
def __init__(self, seed):
self._random = np.random.seed(seed)
self._prev_hand_value = 0
self._current_hand_value = 0
self._history = [[1,1,1], [1,1,1], [1,1,1]]
def next_hand(self):
bet = np.random.randint(self.get_sum(self._current_hand_value))
hand_value = 0
if bet < self._history[self._current_hand_value][0]:
hand_value = 0
elif (bet < self._history[self._current_hand_value][0] + self._history[self._current_hand_value][1]):
hand_value = 1
else:
hand_value = 2
self._prev_hand_value = self._current_hand_value
self._current_hand_value = hand_value
return Hand.get_hand(hand_value)
def get_sum(self, hv):
sum_int = 0
for i in range(3):
sum_int += self._history[hv][i]
return sum_int
def study(self, win):
if win:
self._history[self._prev_hand_value][self._current_hand_value] += 1
else:
self._history[self._prev_hand_value][(self._current_hand_value + 1) % 3] += 1
self._history[self._prev_hand_value][(self._current_hand_value + 2) % 3] += 1
Player class uses a Strategy instance to implement a strategy and decides a next hand or delegates its tasks to a Strategy instance.
# player.py
from strategy import Strategy
class Player:
def __init__(self, name, strategy):
self._name = name
self._strategy = strategy
self._wincount = 0
self._losecount = 0
self._gamecount = 0
def next_hand(self):
return self._strategy.next_hand()
def win(self):
self._strategy.study(True)
self._wincount += 1
self._gamecount += 1
def lose(self):
self._strategy.study(False)
self._losecount += 1
self._gamecount += 1
def even(self):
self._gamecount += 1
def to_string(self):
return f"[{self._name} : {self._gamecount} games, {self._wincount} win, {self._losecount} lose]"
Let’s run the program.
# main.py
from player import Player
from winning_strategy import WinningStrategy
from prob_strategy import ProbStrategy
def main(*args):
print(args)
seed1 = int(args[1])
seed2 = int(args[2])
player1 = Player("Taro", WinningStrategy(seed1))
player2 = Player("Hana", ProbStrategy(seed2))
for _ in range(1000):
next_hand1 = player1.next_hand()
next_hand2 = player2.next_hand()
if next_hand1.is_stronger_than(next_hand2):
print(f"Winner: {player1.to_string()}")
player1.win()
player2.lose()
elif next_hand2.is_stronger_than(next_hand1):
print(f"Winner: {player2.to_string()}")
player1.lose()
player2.win()
else:
print("Even ...")
player1.even()
player2.even()
print("Total result:")
print(player1.to_string())
print(player2.to_string())
if __name__ == "__main__":
main(*sys.argv)
$ python main.py 2 3
Winner: [Hana : 1 games, 0 win, 1 lose]
Even ...
Winner: [Hana : 3 games, 1 win, 1 lose]
Winner: [Taro : 4 games, 1 win, 2 lose]
Even ...
Even ...
Even ...
...
...
...
Even ...
Winner: [Taro : 998 games, 314 win, 358 lose]
Winner: [Hana : 999 games, 358 win, 315 lose]
Total result:
[Taro : 1000 games, 315 win, 359 lose]
[Hana : 1000 games, 359 win, 315 lose]
One benefit of a Strategy pattern: Since any concrete Strategy class is used by a delegation, it is possible to develop an algorithm in that class without modifying a class which delegates its works to a Strategy instance.