diff --git a/.gitignore b/.gitignore index d680d21..b782fd2 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,5 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties +.direnv +.envrc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3641ad1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - "3.4" + - "3.5.0b3" + - "nightly" +# command to install dependencies +install: "pip install -r requirements.txt" +# command to run tests +script: nosetests diff --git a/OO-plan.txt b/OO-plan.txt new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index b44d083..eb58e5b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Game of Sticks +# Game of Sticks [![Build Status](https://travis-ci.org/jwaldrep/game-of-sticks.svg?branch=master)](https://travis-ci.org/jwaldrep/game-of-sticks) ## Description diff --git a/game_of_sticks.py b/game_of_sticks.py new file mode 100644 index 0000000..3160c6d --- /dev/null +++ b/game_of_sticks.py @@ -0,0 +1,283 @@ +import random + + +class SticksGame: + def __init__(self, p1, p2, sticks=100, silent=False): + self.current_player = p1 + self.player_a = p1 + self.player_b = p2 + self.max_draw = 3 + self.player_a.initial = '1' + self.player_b.initial = '2' + self.set_sticks(sticks) + self.player_a.won = None + self.player_b.won = None + self.silent = silent + + def set_sticks(self, sticks=100): + self.sticks = sticks + self.player_a.sticks = sticks + self.player_b.sticks = sticks + if sticks > 3: + self.player_a.max_draw = 3 + self.player_b.max_draw = 3 + else: + self.player_a.max_draw = sticks + self.player_b.max_draw = sticks + + def play(self): + while self.check_winner() is None: + self.next_turn() + return self.check_winner() + + def try_removing_sticks(self): + turn = Turn(self.current_player) + if self.sticks >= turn.removed_sticks: + self.sticks -= turn.removed_sticks + if self.player_a.max_draw > self.sticks: + self.player_a.max_draw = self.sticks + if self.player_a.max_draw > self.sticks: + self.player_b.max_draw = self.sticks + self.player_a.sticks = self.sticks + self.player_b.sticks = self.sticks + if not self.silent: + print('Player {} removes {} sticks. {} sticks remain.'.format( + self.current_player.initial, turn.removed_sticks, + self.sticks)) + return + else: + return self.try_removing_sticks() + + # def start(self): + # self.try_removing_sticks() + def next_turn(self): + self.try_removing_sticks() + if self.current_player == self.player_a: + self.current_player = self.player_b + # turn = Turn(self.current_player) + elif self.current_player == self.player_b: + self.current_player = self.player_a + # turn = Turn(self.current_player) + + def check_winner(self): + # print('checking winner...') + if self.sticks == 0: + self.current_player.won = True + self.other_player().won = False + return self.current_player + return None + + def other_player(self): + # print("it's the other guy") + if self.current_player is self.player_a: + # print(self.player_b) + return self.player_b + return self.player_a + # print(self.player_a) + + +class Player: + def __init__(self, name='Sam', sticks=100): + self.name = name + self.max_draw = 3 + self.initial = 's' + self.sticks = 100 + self.won = None + self.hats = None + + def __repr__(self): + return '{}: {}'.format(self.__class__, self.name) + + def choose(self, auto=None): + if auto is not None: + if auto <= self.max_draw: + return auto + return random.randint(1, self.max_draw) + + def fill_hats(self): + if self.hats is None: + self.hats = [{'in': [1, 2, 3], 'out': []} for _ in + range(self.sticks + 1)] + + def learn(self): + pass + + +class RandomPlayer(Player): + pass + + +class AIPlayer(Player): + def choose(self, auto=None): + if auto is not None: + if auto <= self.max_draw: + return auto + # print('self.sticks: ', self.sticks) + # print('self.hats: ', self.hats) + hat = self.hats[self.sticks] + choice = random.choice(hat['in']) + while choice > self.max_draw: + choice = random.choice(hat['in']) + ball = hat['in'].index(choice) + hat['out'].append(hat['in'].pop(ball)) + # print('RandomPlayer self.hats has {} bins:\n'.format(len(self.hats)), self.hats) + + + return choice + + def learn(self): + # print(self.won) + for sticks, hat in enumerate(self.hats): + if self.won == True: + self.hats[sticks]['in'].extend(hat['out']) + self.hats[sticks]['in'].extend(hat['out']) + self.hats[sticks]['out'] = [] + for i in range(1, 4): + if i not in hat['in']: + self.hats[sticks]['in'].append(i) + + def show_wisdom(self): + print('After learning: ') + print('Sticks\tTake1\tTake2\tTake3') + for sticks, dictionary in enumerate(self.hats): + c = lambda x: str(dictionary['in'].count(x) - 2).replace('-1', '.') + if sticks > 20: + break + print('{}\t{}\t{}\t{}'.format(sticks, c(1), c(2), c(3))) + + +class HumanPlayer(Player): + def choose(self): + choice = input( + "Player {}: {} sticks remain. How many sticks do you take (1-3)?".format( + self.initial, self.sticks)) + if not choice.isnumeric(): + return self.choose() + choice = int(choice) + if choice > self.max_draw: + return self.choose() + return choice + + +class Turn: + def __init__(self, player): + self.player = player + self.removed_sticks = player.choose() + + +class Session: + def __init__(self): + self.game = None + self.sticks = 100 + self.human1 = HumanPlayer('Harry') + self.human2 = HumanPlayer('Sally') + self.npc1 = RandomPlayer('Marvin') + self.npc2 = AIPlayer('R2') + + def h_vs_h(self): + p1 = self.human1 + p2 = self.human2 + self.game = SticksGame(p1, p2, sticks=self.sticks) + self.game.play() + return self.game.check_winner() + + def h_vs_c(self): + p1 = self.human1 + p2 = self.npc2 + self.game = SticksGame(p1, p2, sticks=self.sticks) + p2.fill_hats() + self.game.play() + winner = self.game.check_winner() + p2.learn() + return winner + + def c_vs_c(self, silent=False): + p1 = self.npc1 + p2 = self.npc2 + self.game = SticksGame(p1, p2, sticks=self.sticks, silent=silent) + # p1.fill_hats() + p2.fill_hats() + self.game.play() + winner = self.game.check_winner() + # p1.learn() + p2.learn() + return winner + + def ai_training(self, rounds=1000): + print('Initiating training...', end='') + for i in range(1, rounds + 1): + if rounds % i == 20: + print('.', end='') + self.c_vs_c(silent=True) + +class UserInterface(): + def __init__(self): + self.my_session = Session() + + def num_sticks_menu(self): + try: + sticks = int( + input('How many sticks are on the table? [10-100]:')) + if 10 <= sticks <= 100: + self.my_session.sticks = sticks + return + return self.num_sticks_menu() + except ValueError: + print('Please enter a number between 10 and 100.') + return self.num_sticks_menu() + + def num_player_menu(self): + try: + mode = int(input('How many human players? [0-2]: ')) + except ValueError: + return self.num_player_menu() + if mode == 2: + return self.my_session.h_vs_h() + if mode == 1: + rounds = self.training_menu() + self.my_session.ai_training(rounds) + self.my_session.npc2.show_wisdom() + return self.my_session.h_vs_c() + if mode == 0: + return self.my_session.c_vs_c() + print('Please enter a number between 0 and 2.') + return self.num_player_menu() + + def welcome_menu(self): + print('Welcome to The Game of Sticks!') + + def game_over_menu(self): + print('Player {} wins!'.format( + self.my_session.game.check_winner().initial)) + + def play_again_menu(self): + play_again = input('Play again? [Y/n]: ').lower() + ' ' + return play_again[0] != 'n' + + def training_menu(self): + try: + rounds = int(input( + 'How many rounds of training should the computer get? [10-100000]:')) + if 10 <= rounds <= 100000: + return rounds + except ValueError: + print('Please enter a number between 10 and 100000.') + return self.training_menu() + + def main(self): + self.welcome_menu() + self.num_sticks_menu() + + def loop(): + self.num_player_menu() + self.game_over_menu() + + loop() + while self.play_again_menu(): + loop() + +if __name__ == '__main__': + # my_session = Session() + # my_session.h_vs_h() + my_ui = UserInterface() + my_ui.main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..71094bb --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +coveralls diff --git a/test_game_of_sticks.py b/test_game_of_sticks.py new file mode 100644 index 0000000..d97c4e7 --- /dev/null +++ b/test_game_of_sticks.py @@ -0,0 +1,188 @@ +from game_of_sticks import * + + +def test_game_setup(): + p1 = Player(name='Harry') + p2 = Player(name='Sally') + game = SticksGame(p1, p2, sticks=100) + assert game.sticks == 100 + + +def test_player_has_name(): + player1 = Player(name='Harry') + player2 = Player(name='Sally') + assert player1.name == 'Harry' + assert player2.name == 'Sally' + assert player1 != player2 + + +def test_player_can_choose_num_sticks(): + player1 = Player() + assert player1.choose(auto=3) == 3 + + +class AutoPlayer(Player): + pass + + +class PlayerThatAlwaysPicksThree(Player): + def choose(self): + return 3 + + +def test_first_turn_removes_sticks(): + p1 = PlayerThatAlwaysPicksThree() + p2 = PlayerThatAlwaysPicksThree() + game = SticksGame(p1, p2, sticks=100) + game.next_turn() # start() + assert game.sticks == 97 + + +def test_switch_players(): + p1 = PlayerThatAlwaysPicksThree() + p2 = PlayerThatAlwaysPicksThree() + game = SticksGame(p1, p2, sticks=100) + assert game.current_player == p1 + # start() + game.next_turn() + assert game.current_player == p2 + game.next_turn() + assert game.current_player == p1 + game.next_turn() + assert game.current_player == p2 + + +def test_winner(): + p1 = PlayerThatAlwaysPicksThree() + p2 = PlayerThatAlwaysPicksThree() + game = SticksGame(p1, p2, sticks=100) + game.next_turn() # start() + game.next_turn() + game.next_turn() + game.next_turn() + assert game.check_winner() is None + + game = SticksGame(p1, p2, sticks=6) + game.next_turn() # start() + assert game.sticks == 3 + print('after 1 turn', game.sticks) + game.next_turn() + print('after 2 turns', game.sticks) + assert game.check_winner() == p1 + + game = SticksGame(p1, p2, sticks=9) + game.next_turn() # start() + print('after 1', game.sticks) + game.next_turn() + print('after 2', game.sticks) + game.next_turn() + print('after 3', game.sticks) + assert game.check_winner() == p2 + + +def test_invalid_choice_rejected_gracefully(): + p1 = AutoPlayer() + p2 = AutoPlayer() + random.seed(10) + # This should make choices = 3, 1, 2... + game = SticksGame(p1, p2, sticks=2) + game.next_turn() # game.start() + assert game.sticks == 1 + + +class RepeatingPlayer(Player): + def __init__(self): + super().__init__() + self.choice_sequence = [3, 2, 1] + self.index = 0 + + def choose(self): + if self.index > 2: + self.index = 0 + self.index += 1 + return self.choice_sequence[self.index - 1] + + +def test_repeating_player(): + p1 = RepeatingPlayer() + p2 = RepeatingPlayer() + game = SticksGame(p1, p2, sticks=100) + sticks_history = [100, 97, 94, 92, 90, 89, 88, 85, 82, 80, 78, 77, 76, 73] + # [97,94,91,89,86,85,82,79,76,74] #PTAP3, Rep x3,3,x3,2,x3,1,x3,3,x3,2 + # [98,95,92,90,89,88,86,83,80] # Auto, Repeat 2,3,3,2,1,1,2,3,3, + # Repeating**2 [97, 94, 93, 91, 89, 88, 85, 82] # -3,-3,-1,-2,-2,-1,-3 + # game.start() + score = iter(sticks_history) + assert game.sticks == next(score) + for _ in range(len(sticks_history) - 1): + game.next_turn() + assert game.sticks == next(score) + + +def test_auto_game_outcomes_p1_win(): + p1 = RepeatingPlayer() + p2 = RepeatingPlayer() + game = SticksGame(p1, p2, sticks=12) + sticks_history = [12, 9, 6, 4, 2, 1, 0] + score = iter(sticks_history) + assert game.sticks == next(score) + for _ in range(len(sticks_history) - 1): + game.next_turn() + assert game.sticks == next(score) + assert game.check_winner() == p1 + + +def test_auto_game_outcomes_p2_win(): + p1 = RepeatingPlayer() + p2 = RepeatingPlayer() + game = SticksGame(p1, p2, sticks=11) + sticks_history = [11, 8, 5, 3, 1, 0] + score = iter(sticks_history) + assert game.sticks == next(score) + for _ in range(len(sticks_history) - 1): + game.next_turn() + assert game.sticks == next(score) + assert game.check_winner() == p2 + + +def test_play(): + p1 = RepeatingPlayer() + p2 = RepeatingPlayer() + game = SticksGame(p1, p2, sticks=12) + game.play() + assert game.check_winner() == p1 + + p1 = RepeatingPlayer() + p2 = RepeatingPlayer() + game = SticksGame(p1, p2, sticks=12) + game.play() + assert game.check_winner() == p1 + + +def test_random_c_vs_c_p1_fraction(): + my_session = Session() + my_session.npc1 = RandomPlayer('Marvin') + my_session.npc2 = RandomPlayer('R2') + random.seed() + wins = '' + for _ in range(1000): + winner = my_session.c_vs_c() + wins = wins + winner.initial + assert 400 < wins.count('2') < 600 + + +def test_ai_c_vs_c_p1_fraction(): + my_session = Session() + my_session.sticks = 10 + my_session.npc1 = RandomPlayer('Marvin') + my_session.npc2 = AIPlayer('R2') + random.seed() + wins = '' + for _ in range(10000): + winner = my_session.c_vs_c(silent=True) + wins = wins + winner.initial + print('Sticks\tTake1\tTake2\tTake3') + for sticks, dictionary in enumerate(my_session.npc2.hats): + c = lambda x: dictionary['in'].count(x) + print('{}\t{}\t{}\t{}'.format(sticks, c(1), c(2), c(3))) + assert wins.count('2') > 9000