Pig the dice game/Player: Difference between revisions

From Rosetta Code
Content added Content deleted
(→‎{{header|Python}}: Comment on use of Counter .)
(→‎{{header|Python}}: Add RollTo20 algorithm and better stats.)
Line 53: Line 53:
=={{header|Python}}==
=={{header|Python}}==


There are now two player algorithms, a random player that rolls randomly and the RollTo20 player that rolls if that rounds score is less than 20. Details of the RollTo20 strategy came from a paper referenced from [http://boardgames.about.com/gi/o.htm?zi=1/XJ&zTi=1&sdn=boardgames&cdn=hobbies&tm=55&f=00&su=p284.13.342.ip_p504.6.342.ip_&tt=2&bt=1&bts=1&zu=http%3A//cs.gettysburg.edu/projects/pig/piggame.html here].
The only player at the time of writing is a random player. Player instances are passed full (single) game statistics and so can be more complex in their behaviour.

Player instances are passed full (single) game statistics and so can be more complex in their behaviour.


Notice how Pythons Counter class from the standard library is used to collate the winning statistics near the end of the program without much additional code.
Notice how Pythons Counter class from the standard library is used to collate the winning statistics near the end of the program without much additional code.

<lang python>#!/usr/bin/python3
<lang python>#!/usr/bin/python3


Line 62: Line 65:


This program scores, throws the dice, and plays for an N player game of Pig.
This program scores, throws the dice, and plays for an N player game of Pig.



'''
'''
Line 75: Line 77:
playercount = 2
playercount = 2
maxscore = 100
maxscore = 100
maxgames = 10000
maxgames = 100000




Line 97: Line 99:
'Returns random boolean choice of whether to roll again'
'Returns random boolean choice of whether to roll again'
return bool(random.randint(0, 1))
return bool(random.randint(0, 1))

class RollTo20(Player):
def __call__(self, safe, scores, game):
'Roll again if this rounds score < 20'
return sum(scores) < 20




Line 106: Line 113:
Game.__str__ = game__str__
Game.__str__ = game__str__



def winningorder(players, safescores):
'Return (players in winning order, their scores)'
return tuple(zip(*sorted(zip(players, safescores),
key=lambda x: x[1], reverse=True)))


def playpig(game):
def playpig(game):
'''
'''
Plays the game of pig returning the index of the winning player
Plays the game of pig returning the players in winning order
whilst updating argument game with the details of play.
and their scores whilst updating argument game with the details of play.
'''
'''
players, maxscore, rounds = game
players, maxscore, rounds = game
Line 144: Line 156:
scores, player = [], (player + 1) % playercount
scores, player = [], (player + 1) % playercount


# return winning player and all scores
# return players in winning order and all scores
return players[player], safescore
return winningorder(players, safescore)


if __name__ == '__main__':
if __name__ == '__main__':
Line 152: Line 164:
rounds=[])
rounds=[])
print('ONE GAME')
print('ONE GAME')
print('Winner: %r; Scores: %r\n' % playpig(game))
print('Winning order: %r; Respective scores: %r\n' % playpig(game))
print(game)
print(game)
game = Game(players=tuple(RandPlay(i) for i in range(playercount)),
game = Game(players=tuple(RandPlay(i) for i in range(playercount)),
maxscore=maxscore,
maxscore=maxscore,
rounds=[])
rounds=[])
algos = (RollTo20, RandPlay)
print('\n\nMULTIPLE %r STATISTICS\n for %i GAMES' % (game, maxgames))
print('\n\nMULTIPLE STATISTICS using %r\n for %i GAMES'
winners = Counter(playpig(game._replace(rounds=[]))[0] for i in range(maxgames))
% (', '.join(p.__name__ for p in algos), maxgames,))
winners = Counter(repr(playpig(game._replace(players=tuple(random.choice(algos)(i)
print(' Players(position) in order of winning the most:\n %s' %
for i in range(playercount)),
', '.join(str(r) for r in winners.most_common()))</lang>
rounds=[]))[0])
for i in range(maxgames))
print(' Players(position) winning on left; occurrences on right:\n %s'
% ',\n '.join(str(w) for w in winners.most_common()))</lang>


{{out}}
{{out}}
First is shown the game data for a single game with reduced maxscore then statistics on multiple games.
First is shown the game data for a single game with reduced maxscore then statistics on multiple games.
RollTo20 beats RandPlay on average. It doesn't matter if RollTo20 plays first or second. When both players use the same algorithm there may be an advantage in going first.


<pre>ONE GAME
<pre>ONE GAME
Line 181: Line 198:




MULTIPLE STATISTICS using 'RollTo20, RandPlay'
MULTIPLE Game(players=(RandPlay(0), RandPlay(1)), maxscore=100, rounds=[]) STATISTICS
for 10000 GAMES
for 100000 GAMES
Players(position) in order of winning the most:
Players(position) winning on left; occurrences on right:
(RandPlay(0), 5068), (RandPlay(1), 4932)</pre>
('(RollTo20(1), RandPlay(0))', 25063),
('(RollTo20(0), RandPlay(1))', 24772),
('(RollTo20(0), RollTo20(1))', 13271),
('(RandPlay(0), RandPlay(1))', 12811),
('(RandPlay(1), RandPlay(0))', 12217),
('(RollTo20(1), RollTo20(0))', 11649),
('(RandPlay(0), RollTo20(1))', 115),
('(RandPlay(1), RollTo20(0))', 102)</pre>

Note: ''('(RollTo20(1), RandPlay(0))', 25063)'' means that the algorithm RollTo20 playing as the second player, (1) wins against algorithm RandPlay of the first player, (0) and wins 25063 times. (Zero based indexing so the first player is player(0)).

Revision as of 16:43, 16 September 2012

Pig the dice game/Player is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

The task is to create a dice simulator and scorer of Pig the dice game and add to it the ability to play the game to at least one strategy.

  • State here the play strategies involved.
  • Simulate playing the game a number of times with two players of given strategies and report here summary statistics such as, but not restricted to, the influence of going first or which strategy seems stronger.


Game Rules

The game of Pig is a multiplayer game played with a single six-sided die. The object of the game is to reach 100 points or more. Play is taken in turns. On each person's turn that person has the option of either

  1. Rolling the dice: where a roll of two to six is added to their score for that turn and the player's turn continues as the player is given the same choice again; or a roll of 1 loses the player's total points for that turn and their turn finishes with play passing to the next player.
  2. Holding: The player's score for that round is added to their total and becomes safe from the effects of throwing a one. The player's turn finishes with play passing to the next player.


Reference

J

This is a partial implementation of the current task.

This is a routine to estimate the value of rolling, given the current total of rolls which the player is building (left argument) and the current total of rolls which are a permanent part of the player's score (right argument).

If the expected value is positive, it's probably in the best interest of the player to take the roll. That said, a more sophisticated strategy might play cautiously when a player is sufficiently ahead of the other player(s).

<lang j>pigval=:4 :0

 (+/%#)(-x),}.(1+i.6)<.100-y+x

)</lang>

Examples:

<lang j> 10 pigval 90 _1.66667</lang>

If we have 10 points from our current rolls and have 90 permanent points, rolling again is a bad idea.

<lang j> 0 5 10 15 20 pigval"0/60 65 70 75 80 85 90 95 100

3.33333  3.33333  3.33333   3.33333  3.33333 3.33333  3.33333   3.16667   0
    2.5      2.5      2.5       2.5      2.5     2.5  2.33333 _0.833333  _5
1.66667  1.66667  1.66667   1.66667  1.66667     1.5 _1.66667  _5.83333 _10

0.833333 0.833333 0.833333 0.833333 0.666667 _2.5 _6.66667 _10.8333 _15

      0        0        0 _0.166667 _3.33333    _7.5 _11.6667  _15.8333 _20</lang>

If we have 70 permanent points (or less) we should probably re-roll when our uncommitted rolls total to less than 20.

<lang j> (1+i.19) ([,:1+i:~) +/ 0 < pigval"0/~ 1+i.100

1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19

98 97 96 95 93 92 91 90 89 87 86 85 84 82 81 80 78 77 75</lang>

This is a table of decision points. First row represents sum of our current uncommitted rolls. Second row represents the maximum permanent score where you should roll again with that number of uncommitted points, if we are using this estimation mechanism to choose our actions. Note that the first four columns here should have some obvious validity -- for example, if we have 96 permanent points and we have rolled 4 uncommitted points, we have won the game and we gain nothing from rerolling... Note also that this decision mechanism says we should never reroll if we have at least 20 uncommitted points.

Python

There are now two player algorithms, a random player that rolls randomly and the RollTo20 player that rolls if that rounds score is less than 20. Details of the RollTo20 strategy came from a paper referenced from here.

Player instances are passed full (single) game statistics and so can be more complex in their behaviour.

Notice how Pythons Counter class from the standard library is used to collate the winning statistics near the end of the program without much additional code.

<lang python>#!/usr/bin/python3

See: http://en.wikipedia.org/wiki/Pig_(dice)

This program scores, throws the dice, and plays for an N player game of Pig.

from random import randint from collections import namedtuple import random from pprint import pprint as pp from collections import Counter


playercount = 2 maxscore = 100 maxgames = 100000


Game = namedtuple('Game', 'players, maxscore, rounds') Round = namedtuple('Round', 'who, start, scores, safe')


class Player():

   def __init__(self, player_index):
       self.player_index = player_index
   def __repr__(self):
       return '%s(%i)' % (self.__class__.__name__, self.player_index)
   def __call__(self, safescore, scores, game):
       'Returns boolean True to roll again'
       pass

class RandPlay(Player):

   def __call__(self, safe, scores, game):
       'Returns random boolean choice of whether to roll again'
       return bool(random.randint(0, 1))

class RollTo20(Player):

   def __call__(self, safe, scores, game):
       'Roll again if this rounds score < 20'
       return sum(scores) < 20


def game__str__(self):

   'Pretty printer for Game class'
   return ("Game(players=%r, maxscore=%i,\n  rounds=[\n    %s\n  ])"
           % (self.players, self.maxscore,
              ',\n    '.join(repr(round) for round in self.rounds)))

Game.__str__ = game__str__


def winningorder(players, safescores):

   'Return (players in winning order, their scores)'
   return tuple(zip(*sorted(zip(players, safescores),
                           key=lambda x: x[1], reverse=True)))

def playpig(game):

   
   Plays the game of pig returning the players in winning order
   and their scores whilst updating argument game with the details of play.
   
   players, maxscore, rounds = game
   playercount = len(players)
   safescore = [0] * playercount   # Safe scores for each player
   player = 0                      # Who plays this round
   scores=[]                       # Individual scores this round
   while max(safescore) < maxscore:
       startscore = safescore[player]
       rolling = players[player](safescore, scores, game)
       if rolling:
           rolled = randint(1, 6)
           scores.append(rolled)
           if rolled == 1:
               # Bust! 
               round = Round(who=players[player],
                             start=startscore,
                             scores=scores,
                             safe=safescore[player])
               rounds.append(round)
               scores, player = [], (player + 1) % playercount
       else:
           # Stick
           safescore[player] += sum(scores)
           round = Round(who=players[player],
                         start=startscore,
                         scores=scores,
                         safe=safescore[player])
           rounds.append(round)
           if safescore[player] >= maxscore:
               break
           scores, player = [], (player + 1) % playercount
   # return players in winning order and all scores
   return winningorder(players, safescore)

if __name__ == '__main__':

   game = Game(players=tuple(RandPlay(i) for i in range(playercount)),
               maxscore=20,
               rounds=[])
   print('ONE GAME')
   print('Winning order: %r; Respective scores: %r\n' % playpig(game))
   print(game)
   game = Game(players=tuple(RandPlay(i) for i in range(playercount)),
               maxscore=maxscore,
               rounds=[])
   algos = (RollTo20, RandPlay)
   print('\n\nMULTIPLE STATISTICS using %r\n  for %i GAMES'
         % (', '.join(p.__name__ for p in algos), maxgames,))
   winners = Counter(repr(playpig(game._replace(players=tuple(random.choice(algos)(i)
                                                              for i in range(playercount)),
                                                rounds=[]))[0])
                     for i in range(maxgames))
   print('  Players(position) winning on left; occurrences on right:\n    %s'
         % ',\n    '.join(str(w) for w in winners.most_common()))</lang>
Output:

First is shown the game data for a single game with reduced maxscore then statistics on multiple games. RollTo20 beats RandPlay on average. It doesn't matter if RollTo20 plays first or second. When both players use the same algorithm there may be an advantage in going first.

ONE GAME
Winner: RandPlay(0); Scores: [24, 12]

Game(players=(RandPlay(0), RandPlay(1)), maxscore=20,
  rounds=[
    Round(who=RandPlay(0), start=0, scores=[], safe=0),
    Round(who=RandPlay(1), start=0, scores=[6, 2], safe=8),
    Round(who=RandPlay(0), start=0, scores=[], safe=0),
    Round(who=RandPlay(1), start=8, scores=[], safe=8),
    Round(who=RandPlay(0), start=0, scores=[], safe=0),
    Round(who=RandPlay(1), start=8, scores=[4], safe=12),
    Round(who=RandPlay(0), start=0, scores=[4, 5, 6, 4, 5], safe=24)
  ])


MULTIPLE STATISTICS using 'RollTo20, RandPlay'
  for 100000 GAMES
  Players(position) winning on left; occurrences on right:
    ('(RollTo20(1), RandPlay(0))', 25063),
    ('(RollTo20(0), RandPlay(1))', 24772),
    ('(RollTo20(0), RollTo20(1))', 13271),
    ('(RandPlay(0), RandPlay(1))', 12811),
    ('(RandPlay(1), RandPlay(0))', 12217),
    ('(RollTo20(1), RollTo20(0))', 11649),
    ('(RandPlay(0), RollTo20(1))', 115),
    ('(RandPlay(1), RollTo20(0))', 102)

Note: ('(RollTo20(1), RandPlay(0))', 25063) means that the algorithm RollTo20 playing as the second player, (1) wins against algorithm RandPlay of the first player, (0) and wins 25063 times. (Zero based indexing so the first player is player(0)).