Friday 22 May 2015

Learning Python (5) - Monty Hall

This is another Kata from Emily Bache's book. In essence simulating a game:
There are three doors, behind one is a prize you want (I chose a bicycle), behind the other two something you don't (a goat). You pick a door, but don't see behind it. Then the game reveals the goat behind one of the other two doors. You may then either win what is behind your original choice of door, or swap to the other closed door and win what is behind that. The best strategy is to swap doors, (because probability).

As a programming exercise this is interesting. You have to create and manipulate some state: the prizes, the choice, the open door, the final choice. More importantly there is randomisation: where do the prizes go, which door do you choose, which goat is revealed. Because of the randomisation, the acceptance test of "on average, do you win more often if you swap choice" requires many iterations of the swap / don't swap to be a safe test. So, this Kata is perfect for some use of mocks / fakes in testing to make the smaller tests predictable while still using the code under test which relies on random behaviour.

I got to the solution quite quickly. As an observation of how I'm becoming more comfortable with Python as a tool I got to the end result with less refactoring for code style, which is pleasing. There were still techniques I needed to look up - mostly about the random library and passing methods as arguments.

So, the tests. The fake Game class got created early on, once I'd written a first test against the Game. The fake deals with the random stuff by giving just enough predictability without replacing too much real code. Overriding choosePrizeDoor means that the fake is a really thin shim over the real Game implementation. The reveal is still random, but the initial choice and prize are controlled in the tests, which makes the test predictable.
The method passing into test_swappingIsBetter took a couple of passes to get right, I haven't used this much in Python before, but knew I wanted to use it very quickly as the alternative was too much duplication.
'''
Created on 21 May 2015
@author: Dan Chalmers 
'''

import unittest
from montyhall.prizes import Goat, Bike, Game
import random #ONLY for the swapping is better test! *shudder*

class Test(unittest.TestCase):

    def test_isprize(self):
      prize = Goat()
      self.assertFalse(prize.isWin());
      prize = Bike()
      self.assertTrue(prize.isWin());
      
    def test_getRevealChoiceOfTwo(self):
      doors = FakeGameZero()
      doors.choose(0)
      (doorNum, prize) = doors.reveal()
      self.assertNotEqual(0, doorNum)
      self.assertFalse(prize.isWin())

    def test_getRevealChoiceOfOne(self):
      doors = FakeGameOne()
      doors.choose(0)
      (doorNum, prize) = doors.reveal()
      self.assertEqual(2, doorNum)
      self.assertFalse(prize.isWin())

    def test_swapAndGetPrize(self):
      doors = FakeGameOne()
      doors.choose(0)
      doors.reveal()
      (doorNum, prize) = doors.swapChoice()
      self.assertEqual(1, doorNum)
      self.assertTrue(prize.isWin())
      
    def test_swapAndLoose(self):
      doors = FakeGameZero()
      doors.choose(0)
      doors.reveal()
      (doorNum, prize) = doors.swapChoice()
      self.assertNotEqual(0, doorNum)
      self.assertFalse(prize.isWin())
      
    def test_notSwapAndGetPrize(self):
      doors = FakeGameOne()
      doors.choose(1)
      doors.reveal()
      (doorNum, prize) = doors.keepChoice()
      self.assertEqual(1, doorNum)
      self.assertTrue(prize.isWin())
      
    def test_notSwapAndLoose(self):
      doors = FakeGameZero()
      doors.choose(2)
      doors.reveal()
      (doorNum, prize) = doors.keepChoice()
      self.assertEqual(2, doorNum)
      self.assertFalse(prize.isWin())
      
    '''
    The story test of does swapping mean you win more often.  
    This is the first time with Python that passing a method as a parameter has 
    clearly been the right thing to do. I do like higher order functions!  
    '''
    def test_swappingIsBetter(self):  
      swapCount = self.getWinCount(Game.swapChoice)
      keepCount = self.getWinCount(Game.keepChoice)
      message = "swapped wins: %d,  kept wins: %d" % (swapCount, keepCount)
      self.assertTrue(swapCount > keepCount)
      print(message)
      
    #1000 iterations, to avoid being bitten on the bum by random too frequently (and so seeing a fail)  
    def getWinCount(self, method):
      wins = 0
      for _ in range(1000):
        (_, prize) = method(self.makeGame())
        if prize.isWin(): wins+=1
      return wins
    
    def makeGame(self):
        doors = Game()
        doors.choose(random.randrange(3))
        doors.reveal()
        return doors
      
'''
Initially written as part of refactoring of the first test of Game
I had in mind that a mock / fake was part of what I wanted to explore in this Kata, 
but in the end this was a natural way to make a reliable test from a class which normally has some random behaviour
'''
class FakeGameZero(Game):
  def _choosePrizeDoor_(self):
    return 0
  
class FakeGameOne(Game):
  def _choosePrizeDoor_(self):
    return 1
  
if __name__ == "__main__":
    unittest.main()
        

The code is quite straightforward. Two classes to represent the prizes. In Java this would probably have come out as an enum and I'm still not convinced that this is the nicest approach in Python. I avoided having all the state as separate variables in the Game for a while, but in the end having the three variables led to much simpler methods. As state was unavoidable, going with the flow seemed to be the way to go.
'''
Created on 21 May 2015
The Monty Hall Game Kata after Emily Bache's description
@author: Dan Chalmers 
'''

import random

class Goat(object):
  def isWin(self):
    return False
  
class Bike(object):
  def isWin(self):
    return True
  
  
class Game(object):
  DOOR_COUNT = 3
  
  def __init__(self):
    self.doors = [Goat()] * self.DOOR_COUNT
    self.choice = None
    self.revealed = None
    #self.prizeDoor = None #prize door is set next
    self._setPrize_(self._choosePrizeDoor_())
    
  def choose(self, doorNum):
    self.choice = doorNum
      
  def _choosePrizeDoor_(self):
    return random.randrange(self.DOOR_COUNT)
    
  def _setPrize_(self, prizeDoor):
    self.prizeDoor = prizeDoor
    self.doors[prizeDoor] = Bike()
    
  def reveal(self):
    cands = [x for x in range(self.DOOR_COUNT) if x != self.choice and x != self.prizeDoor]
    self.revealed = cands[random.randrange(len(cands))]
    return (self.revealed, self.doors[self.revealed])
  
  def getPrizeDoorNum(self):
    return self.prizeDoor
  
  def swapChoice(self):
    cands = [x for x in range(self.DOOR_COUNT) if x != self.choice and x != self.revealed]
    self.choice = cands[0]
    return self._getChoice_()
  
  def keepChoice(self):
    return self._getChoice_()
    
  def _getChoice_(self):
    return (self.choice, self.doors[self.choice])

This was quite a quick problem to solve, but still taught me new things (random, passing functions). I was pleased with how easy using the fake was in testing. I thought it worked well as a follow-on from Medicine Clash, practising the list comprehensions and use of classes.

No comments:

Post a Comment