From a8a869b413334a39e6d3792acfd9faa6be4faaf7 Mon Sep 17 00:00:00 2001 From: Andrew Armbruster Date: Sun, 20 Aug 2023 15:55:19 +0200 Subject: [PATCH] feat: add sequence strategy Add a play strategy that iterates through a pre-defined sequence of plays. It is available via command line option, though for now just uses the default sequence when specified via command line. --- README.md | 10 ++++++-- rps-sim.py | 23 ++++++++++++++--- rps/strategy.py | 28 +++++++++++++++++++++ test/test_strategy.py | 58 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 111 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 11abd60..0c6fee3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RPS Sim -A small console program that 'simulates' games of rock paper scissors. +A small console program that simulates games of rock paper scissors. ## Installing @@ -39,10 +39,16 @@ Once installed, you should be able to run it with: $ python ./rps-sim.py ``` -You can specify the number of games that should be played via the `--games` argument, e.g.: +You can specify the number of games that should be played via the `--games` option, e.g.: ```shell $ python ./rps-sim.py --games 7 ``` + +You can specify the strategy that a player will use via the `--p1-strategy`/`--p2-strategy` option, e.g.: +```shell +$ python ./rps-sim.py --p1-strategy 'sequence' +``` + Run `python ./rps-sim.py --help` for a complete list of options ## Tests diff --git a/rps-sim.py b/rps-sim.py index d2a89fb..7506591 100644 --- a/rps-sim.py +++ b/rps-sim.py @@ -1,7 +1,8 @@ +from typing import Literal import click from rps.player import BotPlayer from rps.play import human_readable -from rps.strategy import RandomPlayStrategy +from rps.strategy import get_strategy, Strategy from rps.game import play_games @@ -14,9 +15,23 @@ ) @click.option("--p1-name", default="Player 1", help="Name of first player") @click.option("--p2-name", default="Player 2", help="Name of second player") -def simple_sim(games: int, p1_name: str, p2_name: str) -> None: - player1 = BotPlayer(RandomPlayStrategy(), p1_name) - player2 = BotPlayer(RandomPlayStrategy(), p2_name) +@click.option( + "--p1-strategy", + default=Strategy.RANDOM, + type=click.Choice([Strategy.RANDOM, Strategy.SEQUENCE]), + help="Strategy for player 1", +) +@click.option( + "--p2-strategy", + default=Strategy.RANDOM, + type=click.Choice([Strategy.RANDOM, Strategy.SEQUENCE]), + help="Strategy for player 2", +) +def simple_sim( + games: int, p1_name: str, p2_name: str, p1_strategy: Strategy, p2_strategy: Strategy +) -> None: + player1 = BotPlayer(get_strategy(p1_strategy), p1_name) + player2 = BotPlayer(get_strategy(p2_strategy), p2_name) results = play_games(player1, player2, games) for result in results: diff --git a/rps/strategy.py b/rps/strategy.py index 8e4996b..a3fc12e 100644 --- a/rps/strategy.py +++ b/rps/strategy.py @@ -1,6 +1,8 @@ import abc from random import randrange +from typing import Any, Sequence from .play import Play +from enum import auto, StrEnum class PlaySelectionStrategy(object): @@ -11,6 +13,32 @@ def play(self) -> Play: """Required method""" +class Strategy(StrEnum): + RANDOM = auto() + SEQUENCE = auto() + + class RandomPlayStrategy(PlaySelectionStrategy): def play(self) -> Play: return Play(randrange(Play.min(), Play.max() + 1)) + + +default_sequence = [Play.ROCK, Play.PAPER, Play.SCISSORS] + + +class SequencePlayStrategy(PlaySelectionStrategy): + def __init__(self, sequence: Sequence[Play] = default_sequence): + self._sequence = sequence if sequence is not None else default_sequence + self._idx = 0 + + def play(self) -> Play: + item = self._sequence[self._idx] + self._idx = (self._idx + 1) % len(self._sequence) + return item + + +def get_strategy(strategy: Strategy, args: Any = None) -> PlaySelectionStrategy: + if strategy is Strategy.RANDOM: + return RandomPlayStrategy() + elif strategy is Strategy.SEQUENCE: + return SequencePlayStrategy(sequence=args) diff --git a/test/test_strategy.py b/test/test_strategy.py index 390b3ce..5e3994b 100644 --- a/test/test_strategy.py +++ b/test/test_strategy.py @@ -1,8 +1,62 @@ -from rps.strategy import RandomPlayStrategy +from rps.strategy import ( + RandomPlayStrategy, + SequencePlayStrategy, + Strategy, + get_strategy, +) from rps.play import Play class TestStrategy: - def test_RandomPlayStrategy_returns_play(self): + def test_RandomPlayStrategy_returns_play(self) -> None: sut = RandomPlayStrategy() assert type(sut.play()) is Play + + def test_SequencePlayStrategy_returns_sequence(self) -> None: + sequence = [Play.PAPER, Play.ROCK, Play.SCISSORS] + sut = SequencePlayStrategy(sequence) + first_play = sut.play() + + assert first_play == sequence[0] + assert sut.play() == sequence[1] + assert sut.play() == sequence[2] + + def test_SequencePlayStrategy_repeats_sequence_on_exhaustion(self) -> None: + sequence = [Play.PAPER, Play.ROCK] + sut = SequencePlayStrategy(sequence) + first_play = sut.play() + + assert first_play == sequence[0] + assert sut.play() == sequence[1] + assert sut.play() == sequence[0] + assert sut.play() == sequence[1] + assert sut.play() == sequence[0] + assert sut.play() == sequence[1] + + def test_SequencePlayStrategy_has_default_sequence(self): + sut = SequencePlayStrategy() + first_play = sut.play() + assert first_play is not None + second_play = sut.play() + assert second_play is not None + assert second_play != first_play + third_play = sut.play() + assert third_play is not None + assert third_play != second_play + assert third_play != first_play + + def test_get_strategy_returns_strategy(self): + returned = get_strategy(Strategy.RANDOM) + assert type(returned) is RandomPlayStrategy + + def test_get_strategy_returns_strategy_with_args(self): + returned = get_strategy(Strategy.SEQUENCE, args=[Play.ROCK, Play.SCISSORS]) + assert type(returned) is SequencePlayStrategy + first_play = returned.play() + assert first_play == Play.ROCK + + def test_get_strategy_returns_strategy_with_args_when_defer_defaults(self): + returned = get_strategy(Strategy.SEQUENCE) + assert type(returned) is SequencePlayStrategy + first_play = returned.play() + assert first_play == Play.ROCK