Design Patters: Strategy

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.

Leave a comment