Saturday, January 3, 2009

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:

  © Blogger template 'Minimalist G' by Ourblogtemplates.com 2008

Back to TOP