ProjectEuler#84 : monopoly game in Scala
In the last weeks, I slowed down my completion rate of Project Euler's problems a lot. But yesterday, looking for something to do for the last days of Christmas's holidays, I found the #84 problem which seemed quite fun: the goal is to find what are the 3 most visited squares in the game if we played with 4 sided dices in place of the common 6 sided ones.
It's interesting, because it's a probabilistic problem, and I'm lame in proba. So, as the problem does not require too much precision (at contrary to problem #213...), I could solve it in a monte-carlo way. This is the kind of proba solving method that suits me the best, because no proba are used...
As always, it was really simple to express the solution I imagine in Scala, and that's the result.
Be careful ! If you play at Project Euler, the following contains a solution that you may prefer to find by yourself !
/* * This is the companion object of our Monopoly class, it's the * place where we put all the common and side stuff, * the board definition and our Random number generator * definition, for example. */ object Monopoly { /* * The monopoly board * (yes, we could have just use the square numbers, * but I found that clearer at the begining, and the overhead is * really small) */ val board = Array( "GO" , "A1", "CC1", "A2" , "T1", "R1", "B1" , "CH1", "B2", "B3", "JAIL", "C1", "U1" , "C2" , "C3", "R2", "D1" , "CC2", "D2", "D3", "FP" , "E1", "CH2", "E2" , "E3", "R3", "F1" , "F2" , "U2", "F3", "G2J" , "G1", "G2" , "CC3", "G3", "R4", "CH3", "H1" , "T2", "H2" ) /* * Match a case name to it's number */ val reverseBoard = { var m = Map[String,Int]() for(i <- 0 until board.size) m = m.+((board(i) , i)) m } abstract class Random { def next : Int } /* * A simple homothetie from int's range to a new one range, * used to transform Random to dice roll */ def randomInRange(start:Int,end:Int, random: Random) : Int = { //that was funny to set up, there was some intersting //overflow and int division to take care of ;) val i = start + ((random.next.toFloat - Int.MinValue) * (end+1 - start) / (Int.MaxValue.toFloat - Int.MinValue) ).toInt if(i > end) end else if(i} /* * Utility methods that sends three square back */ def goBack3(square:String) : String = { val i = reverseBoard(square) if(i>2)board(i-3) else board(board.size+i-4) } /* * Utility method that given a square, * return the next Railway Company square */ def gotoNextR(square:String) : String = { val i = reverseBoard(square) if(i>5 && i <= 15) "R2" else if(i>15 && i <= 25) "R3" else if(i>25 && i <= 35) "R4" else "R1" } /* * Utility method that given a square, * return the next Utility company square */ def gotoNextU(square:String) : String = { val i = reverseBoard(square) if(i > 12 && i <= 28) "U2" else "U1" } /* * Represent a stack of cards, used for Communty cards and * Chance. * We cycle throught an array to convey the idea that cards * are put back on the stack after use. * * Each "card" is a function that is awaiting for the real * square name to return the resulting square move. Most of * the card does nothing for us, so the function is identity * (return the square itself) */ class CardStack(random: Random, private val size:Int) { protected val cards = new Array[String=>String](size) for(i <- 0 until size) cards(i) = (square:String) => square private var index = 0 private def rand = random def next(square:String) : String = { index = (index+1)%size cards(index)(square) } } /* * The class that represents the chance card heap. * Fairly random card distribution, * see http://xkcd.com/221/ for more details */ class CH(random: Random) extends CardStack(random,16) { cards(1) = gotoNextR _ cards(2) = goBack3 _ cards(4) = _ => "GO" cards(5) = _ => "R1" cards(7) = _ => "C1" cards(9) = _ => "H2" cards(10) = gotoNextR _ cards(12) = _ => "JAIL" cards(14) = gotoNextU _ cards(15) = _ => "E3" } /* * The class that represent the community chest card heap. * Fairly random card distribution, * see http://xkcd.com/221/ for more details (again) */ class CC(random: Random) extends CardStack(random,16) { cards(4) = _ => "GO" cards(10) = _ => "JAIL" } /* * A class that represents a pair of dices * with a given number of faces * * The only intersting thing to do with * is of course to roll them * (with cards, dices and money involved, I can't * understand where The Monopoly failed to be attractive). */ class Dices(faces:Int,random: Random) { def roll = ( randomInRange(1,faces,random), randomInRange(1,faces,random) ) } } /* * That's the actuall Monopoly representation. * In our simplification of the Game, a * Monopoly has a pair of dices, a Community Chest * and a Chance heap of cards, a log of the number * of consecutive doubles, and a marker for the * current position of the player (notice that as * there is only one player, the game should be * even less attractive than the real Mo,opoly...) * * Nonetheless, we want to play to our game, so * we have a "turn" method that roll the dices and * move the player thanks to a "next square" method * processor. * * And that's all, our monopoly really looks like * the real ! */ class Monopoly(faces:Int,random: Monopoly.Random) { import Monopoly.{CC,CH,Dices} import Monopoly.{board=>B,reverseBoard=>RB} val dices = new Dices(faces,random) val cc = new CC(random) val ch = new CH(random) var doubles = 0 var currentPosition = 0 private def next(square:String) : String = { val s = square match { case "CC1" => cc.next(square) case "CC2" => cc.next(square) case "CC3" => cc.next(square) case "CH1" => ch.next(square) case "CH2" => ch.next(square) case "CH3" => ch.next(square) case "G2J" => "JAIL" case _ => square } if(s == square) s else next(s) } /* * A turn starts at one square and end on another, * perhaps passing on several other squares */ def turn() : String = { val d = dices.roll var newSquare = next( B((currentPosition+d._1+d._2)%B.size )) //3 consecutive double send to Jail if(d._1 == d._2) { doubles = doubles+1 if (doubles >= 3) { doubles = 0 newSquare = "JAIL" } } else doubles = 0 currentPosition = RB(newSquare) newSquare } } /* * Now that we can have a Monopoly, * we game play a party ! * In Scala, party are called "application", * but it's because software developper are * known to be dull people. */ object Problem85 extends Application { import Monopoly.{reverseBoard=>RB} /* * We want to observe the evolution of * the game, so we define a Logger * (I'm sooooo unsurprising) */ abstract class Logger { def register(square:String) } /* * A looger that just output to console, * used at the begining, to see the player * evolves... */ trait ConsoleLogger extends Logger { override def register(s:String) = println(s) } /* * ... and was quickly replaced by a logger that * just keep everything in a map, not that * it was boring... Well, actually, it was. */ trait MapLogger extends Logger { var map = new HashMap[String,Int]() override def register(s:String) { /* * ** Note to Java developpers ** * look how it's easy to say * "I want to retrieve a key value * from my map, but if the key does * not exists, defined it with the * given default value" in Scala. * Just think to the number of if, and * brackets and the like in Java... */ map(s) = map.getOrElse(s,0) + 1 } } /* * Aaaaaahhhhhhh ! At last, there is our Game ! * It's defined with a random generator, the number * of face we want our dices to have (remember, * that's actually the point of the problem...), and * a limit number. * * The limit number is the number of turn that our * game will play before stoping (and we, look to * the results). As we are in a Monte Carlo similution, * this number will have to be HUGE. Say ONE MILLION ! * Ok, that's ridiculously small, but thanks to the * kindness of project Euler team, the number converge * quickly, and my PC is not as fast as clusters used * for real Monte Carlo simulation, * so it will have to do ;) */ abstract class Game( faces:Int, random: Monopoly.Random, limit:Int) extends Logger { val monopoly = new Monopoly(faces,random) var square = "" var turn = 0 def start = { while(turn < limit) { this.register(monopoly.turn()) turn = turn + 1 } } } /* * Oups, don't forget to actually get a random * generator. As explained for the Game, we don't * need something too sophisticated for this * simple example. We may have taken * http://www.honeylocust.com/RngPack/ for a * better pseudo-random generator (for example). */ class JavaSimpleRandom extends Monopoly.Random { val r = new scala.util.Random() override def next = r.nextInt } /* ============================= */ /* Actual test */ /* ============================= */ val r = new JavaSimpleRandom() val rounds = 1000000 val test = new Game(4,r,rounds) with MapLogger print("begins... ") test.start println("results:") val a = test.map.toList.sort( ( x:(String,Int), y:(String,Int) ) => x._2 > y._2) for(i <- 0 until 5) { println("%5s (%02d): %s". format( a(i)._1, RB(a(i)._1), a(i)._2.toFloat/rounds) ) } }
Voilà !
0 comments:
Post a Comment