Sunday, January 25, 2009

Scala Mock Interview: OO Design

In my last post, I described some of my Scala based solutions to Steve Yegge's phone screen questions. This week, to address the OO Design section, I attempt an Object-Oriented implemention of the popular board game Monopoly. This was an actual question asked of me at my Amazon interview, one that I bungled hopelessly because I realized about 30 minutes into a 45 minute interview that I had forgotten the rules of the game.

Another reason is that my boys got the game last Christmas, so I figured it would be a good chance to re-learn it. About 6.5 hours into the game, we had to terminate it because the kids had homework to do. Made me wonder how long the game would last if we had kept going. Also, thinking about it some more, it seemed to be an interesting OO model, and implementing it in Scala would be a good way of learning the language a bit more.

I ended up with an object model, the UML diagram for which is shown below. To be honest, this is the final product after about a week (approximately 2 hours per day) of iterative design and coding, although the object model I started with contained about 80% of the classes you see in here.

The central class in the system is the Monopoly singleton object. It contains 40 slots (represented by objects of trait Slot), of which 28 are properties, and the rest represent some sort of action that the player landing on the spot has to do, represented by the Asset trait and the ActionSlot class respectively. Among the 28 properties, there are 4 railroads and 2 utilities, and the rest represent locations. The difference between locations and the other properties is that locations can be built on (improved). So Assets are subclassed into PropertySlot, RailroadSlot and UtilitySlot. The reason RailroadSlot and UtilitySlot are broken out is that the method to calculate rent is different for them.

In addition, there is a stack of Chance and Community Chest cards. On reaching certain slots, a player picks the top card and executes the action described in the card, so both Chance and Community Chests can be represented by a List of ActionSlots.

Moving on to more (sort of) animate entities, the players interact with the Monopoly object by rolling dice, moving through the slots, etc. They also interact with each other and the Bank by buying and selling property. The Bank is a participant in transactions, but does not actively buy and sell property. So our solution is to create a Participant trait which participates in transactions, which both the Player class (with methods to roll dice, move, buy, sell, liquidate, etc) and the Bank object (since there is only a single instance of this in our system) extend.

The Scala code for this system is shown below. The code is quite self-explanatory, and I have comments where I felt that things needed explaining. If you don't understand something, I suggest checking out the Programming Scala book or looking the thing up on the Internet. I've been doing a lot of both this last week, and I can tell you that its been quite an education. The code is broken up into three files, representing the three layers in the UML diagram above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Source: src/main/scala/com/mycompany/smi/ood/Monopoly.scala
package com.mycompany.smi.ood

import scala.io.Source
import scala.collection.mutable.Map

object Monopoly {

  // initialize data structures from the config file
  val lines = 
    Source.fromFile("src/main/resources/monopoly.cfg").getLines.toList
  val slots = lines.filter(line => line.startsWith("slot")).
    map(line => {
      val cols = line.split(":")
      cols(2) match {
        case "Property" => new PropertySlot(cols(3), cols(4),
                                            cols(5).toInt, cols(6).toInt,
                                            cols(7).toInt);
        case "Railroad" => new RailroadSlot(cols(3), cols(4).toInt)
        case "Utility" => new UtilitySlot(cols(3), cols(4).toInt)
        case "Action" => new ActionSlot(cols(3))
      }
    }).toArray
  var chanceCards = shuffle(
    lines.filter(line => line.startsWith("chance")).
    map(line => new ActionSlot(line.split(":")(2))))
  var communityCards = shuffle(
    lines.filter(line => line.startsWith("community")).
    map(line => new ActionSlot(line.split(":")(2))))
  var players = List[Player]()

  // client code will add as many players that will play
  def addPlayer(player: Player): Player = {
    players ::= player
    player
  }

  // All players throw the dice, the player with the highest throw
  // starts first. We simulate this here with a shuffle.
  def startPlay(): Unit = {
    val bank = Bank
    players = shuffle(players)
  }

  // convenience method for client code to get a list of slots of a
  // particular type
  def slotsOfType[T](clazz: Class[_ <: T]): List[T] = {
    slots.filter(slot => clazz.isAssignableFrom(slot.getClass)).
      map(slot => slot.asInstanceOf[T]).toList
  }

  // Fisher-Yates shuffle adapted from:
  // http://jdleesmiller.blogspot.com/2008/12/shuffles-surprises-and-scala.html
  def shuffle[T](list: List[T]): List[T] = {
    val randomizer = new Random()
    var arr = list.toArray
    for (i <- arr.indices.reverse) {
      val temp = arr(i)
      val j = randomizer.nextInt(i + 1)
      arr(i) = arr(j)
      arr(j) = temp
    }
    arr.toList
  }
}

The Monopoly object is configured with data from a configuration file, which is just a colon delimited file as shown below. The comment lines at the top of each block describe the contents.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# Source: src/main/resources/monopoly.cfg
# slot:${id}:Action:${name}:
# slot:${id}:Property:${name}:${group}:${sellingPrice}:${rent}:${housePrice}:
# slot:${id}:Railroad:${name}:${sellingPrice}:
# slot:${id}:Utility:${name}:${sellingPrice}:
slot:1:Action:AdvanceToGo:
slot:2:Property:Mediterenean Avenue:Brown:60:2:50:
slot:3:Action:PickCommunityCard:
slot:4:Property:Baltic Avenue:Brown:60:4:50:
slot:5:Action:IncomeTax:
slot:6:Railroad:Reading Railroad:200:
slot:7:Property:Oriental Avenue:LtBlue:100:6:50:
slot:8:Action:PickChanceCard:
slot:9:Property:Vermont Avenue:LtBlue:100:6:50:
slot:10:Property:Connecticut Avenue:LtBlue:120:8:50:
slot:11:Action:GoToJail:
slot:12:Property:St Charles Place:Violet:140:10:100:
slot:13:Utility:Electric Company:150:
slot:14:Property:States Avenue:Violet:140:10:100:
slot:15:Property:Virginia Avenue:Violet:160:12:100:
slot:16:Railroad:Pennsylvania Railroad:200:
slot:17:Property:St James Place:Orange:180:14:100:
slot:18:Action:PickCommunityCard:
slot:19:Property:Tennessee Avenue:Orange:180:14:100:
slot:20:Property:New York Avenue:Orange:200:16:100:
slot:21:Action:FreeParking:
slot:22:Property:Kentucky Avenue:Red:220:18:150:
slot:23:Action:PickChanceCard:
slot:24:Property:Indiana Avenue:Red:220:18:150:
slot:25:Property:Illinois Avenue:Red:240:18:150:
slot:26:Railroad:B&O Railroad:200:
slot:27:Property:Atlantic Avenue:Yellow:260:22:150:
slot:28:Property:Ventnor Avenue:Yellow:260:22:150:
slot:29:Utility:Water Works:150:
slot:30:Property:Marvin Gardens:Yellow:280:22:150:
slot:31:Action:GoToJail:
slot:32:Property:Pacific Avenue:Green:300:26:200:
slot:33:Property:North Carolina Avenue:Green:300:26:200:
slot:34:Action:PickCommunityCard:
slot:35:Property:Pennsylvania Avenue:Green:300:28:200:
slot:36:Railroad:Short Line Railroad:200:
slot:37:Action:PickChanceCard:
slot:38:Property:Park Place:DkBlue:350:35:200:
slot:39:Action:LuxuryTax:100:
slot:40:Property:Boardwalk:DkBlue:400:50:200:

# chance:${id}:${actionName}:
chance:1:AdvanceToGo:
chance:2:AdvanceToIllinoisAve:
chance:3:AdvanceToNearestUtility:
chance:4:AdvanceToNearestRailroad:
chance:5:AdvanceToStCharlesPlace:
chance:6:BankDividend:
chance:7:GetOutOfJailFree:
chance:8:Back3Spaces:
chance:9:GoToJail:
chance:10:OperaTickets:
chance:11:SpeedingFine:
chance:12:AdvanceToReadingRailroad:
chance:13:AdvanceToBoardwalk:
chance:14:LoanMatures:
chance:15:WonCrosswordComp:
chance:16:DrunkFine:

# community:${id}:${actionName}:
community:1:AdvanceToGo:
community:2:BankError:
community:3:DoctorsFee:
community:4:GetOutOfJailFree:
community:5:GoToJail:
community:6:Birthday:
community:7:IncomeTaxRefund:
community:8:HospitalFees:
community:9:ConsultingFees:
community:10:StreetRepairs:
community:11:BeautyContest:
community:12:Inheritance:
community:13:StockSale:
community:14:HolidayFund:
community:15:PayFine:
community:16:InsurancePremium:

The classes and traits for the Slot hierarchy is detailed in the Slot.scala file below:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// Source: src/main/scala/com/mycompany/smi/ood/Slot.scala
package com.mycompany.smi.ood

trait Slot {
  val name: String

  override def hashCode(): Int = {
    name.hashCode
  }

  override def equals(that: Any): Boolean = {
    that.isInstanceOf[Slot] && this.name == that.asInstanceOf[Slot].name
  }

  override def toString(): String = {
    name
  }
}

trait Asset extends Slot {
  val sellingPrice: Int
  val mortgagePrice: Int = sellingPrice / 2

  var ownedBy: Participant = Bank
  var mortgaged: Boolean = false

  def rent(): Int
}

class PropertySlot(propertyName: String, propertyGroup: String,
                   value: Int, rentValue: Int, houseValue: Int) extends Asset {
  val name = propertyName
  val group = propertyGroup
  val sellingPrice = value
  val housePrice = houseValue
  val hotelPrice = houseValue * 4
  var houses = 0
  var hotels = 0

  def rent() = (value / 10)

  override def toString(): String = {
    name + "/" + group + "(" + houses + "," + hotels + ")"
  }
}

class RailroadSlot(railroadName: String, value: Int) extends Asset {
  val name = railroadName
  val sellingPrice = value

  // if owner owns (1,2,3,4) railroads, then charge (25,50,100,200)
  def rent: Int = {
    Monopoly.slotsOfType[RailroadSlot](classOf[RailroadSlot]).
      filter(slot => slot.ownedBy == this.ownedBy).size * 25
  }
}

class UtilitySlot(utilityName: String, value: Int) extends Asset {
  val name = utilityName
  val sellingPrice = value

  // we've modified this a bit. The rule says that the incoming player must
  // roll the dice and pay 4xdice if 1 utility owned, else 10x dice if both
  // owned. Since the dice throw is really the sum of two random numbers
  // generated between 1-6, it does not matter who throws it, and it is more
  // convenient for us to get a reference to the railroad owning player, so
  // we make the owner throw the dice to compute the rent.
  def rent: Int = {
    val dicethrow = ownedBy.asInstanceOf[Player].rollDice()
    val utilsOwned = Monopoly.slotsOfType[UtilitySlot](classOf[UtilitySlot]).
      filter(slot => slot.ownedBy == this.ownedBy).size
    if (utilsOwned == 1) (4 * dicethrow) else (10 * dicethrow)
  }
}

trait Action {
  def execute(player: Player)
}

class ActionSlot(actionName: String) extends Slot with Action {
  val name = actionName
  override def execute(player: Player) {
    println("...Executing: " + name)
    name match {
      case "AdvanceToBoardwalk" => player.advanceTo(39)
      case "AdvanceToGo" => player.advanceTo(0)
      case "AdvanceToIllinoisAve" => player.advanceTo(25)
      case "AdvanceToNearestRailroad" => 
        player.advanceToNearestOf(List(5, 15, 25, 35))
      case "AdvanceToNearestUtility" => 
        player.advanceToNearestOf(List(12, 28))
      case "AdvanceToReadingRailroad" => player.advanceTo(5)
      case "AdvanceToStCharlesPlace" => player.advanceTo(11)
      case "Back3Spaces" => player.advanceBy(-3)
      case "BankDividend" => Bank.pay(player, 50)
      case "BankError" => Bank.pay(player, 200)
      case "BeautyContest" => Bank.pay(player, 10)
      case "Birthday" => Bank.payOtherPlayers(player, 50)
      case "ConsultingFees" => Bank.pay(player, 25)
      case "DoctorsFee" => player.pay(Bank, 50)
      case "DrunkFine" => player.pay(Bank, 20)
      case "GetOutOfJailFree" => player.hasGetOutOfJailFreeCard = true
      case "GoToJail" => player.advanceToNearestOf(List(10, 30))
      case "HolidayFund" => Bank.pay(player, 100)
      case "HospitalFees" => player.pay(Bank, 100)
      case "IncomeTax" => player.pay(Bank, 200)
      case "IncomeTaxRefund" => Bank.pay(player, 20)
      case "Inheritance" => Bank.pay(player, 100)
      case "InsurancePremium" => player.pay(Bank, 50)
      case "LoanMatures" => Bank.pay(player, 150)
      case "LuxuryTax" => player.pay(Bank, 100)
      case "OperaTickets" => Bank.otherPlayersPay(player, 50)
      case "PayFine" => player.pay(Bank, 10)
      case "PickChanceCard" => player.pickCard(Monopoly.chanceCards)
      case "PickCommunityCard" => player.pickCard(Monopoly.communityCards)
      case "SpeedingFine" => player.pay(Bank, 15)
      case "StreetRepairs" => player.pay(Bank, player.computeRepairCharge)
      case "StockSale" => Bank.pay(player, 45)
      case "WonCrosswordComp" => Bank.pay(player, 100)
      case _ =>
    }
  }
}

Similarly, the Player.scala file contains traits and classes for the Participant hierarchy.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// Source: src/main/scala/com/mycompany/smi/ood/Player.scala
package com.mycompany.smi.ood

import scala.util.Random
import scala.collection.mutable.Map

trait Participant {
  var cash: Int

  def pay(target: Participant, amount: Int): Int = {
    if ((cash - amount) > 0) {
      target.receive(amount)
    }
    cash = cash - amount
    cash
  }

  private def receive(amount: Int): Int = {
    cash = cash + amount
    cash
  }
}

class Player(playerName: String) extends Participant {

  var name = playerName
  var currentPosition = 0       // start position 0-based
  var cash = 1500               // starting cash balance
  var hasGetOutOfJailFreeCard = false
  var bankrupt = false

  def rollDice(): Int = {
    val random = new Random()
    val d1 = random.nextInt(6) + 1
    val d2 = random.nextInt(6) + 1
    println("...Roll: (" + d1 + "+" + d2 + ")=" + (d1+d2))
    d1 + d2
  }

  // this is used to respond to rules such as advance to the "nearest"
  // railroad or utility. It checks which of these is "closer" to the
  // player's current position and gets there. In case it is "ahead" of 
  // both slots, then this means that it passes Go so we should collect 
  // the salary.
  def advanceToNearestOf(slots: List[Int]): Int = {
    val slotsAhead = slots.filter(slot => (slot - currentPosition) > 0)
    val nearestSlot = if (slotsAhead.isEmpty) {
      // this will involve passing go, so collect $200
      Bank.pay(this, 200)
      slots(0)
    } else slotsAhead(0)
    advanceTo(nearestSlot)
  }

  // advance by the score specified. As before, we check to see if we pass Go
  // so we know to collect the $200 salary if we do.
  def advanceBy(numSlots: Int): Int = {
    val newPosition = numSlots + currentPosition
    currentPosition = if (newPosition >= 40) {
      // this passed go, so collect $200
      Bank.pay(this, 200)
      newPosition - 40
    } else if (newPosition < 0) {
      newPosition + 40
    } else {
      newPosition
    }
    advanceTo(currentPosition)
  }

  // advance to a specific slot position. This is used to handle commands
  // such as GoToIllinoisAvenue, etc. Although this method is delegated to by
  // both the other advanceTo* methods, we repeat the check for Passing Go
  // because each method can be called independently and the check must be
  // present in all these methods. The checks do not interfere with each
  // other, even though they appear to. For example, the Bank.pay call in
  // this method will never be fired if a sanitized slot id is passed in,
  // such as those from the calls from the other advanceTo* methods.
  def advanceTo(slot: Int): Int = {
    Bank.pay(this, 
      if (currentPosition < 0 || currentPosition > slot) 200 else 0)
    currentPosition = slot
    currentPosition
  }

  // pick a card from the named stack, execute the action specified in
  // the card, then put the card back at the bottom of the stack. In a real
  // game, a "get out of jail free" card is held until it is used, but since
  // it can come from either a chance or a community chest stack, it is hard
  // to figure out where it should be returned, so we return it immediately
  // here, but update a flag in the player so he can use it when required.
  def pickCard(cards: List[ActionSlot]): List[ActionSlot] = {
    val action = cards.head
    action.execute(this)
    (action :: cards.tail.reverse).reverse
  }

  // buy an asset from the bank. Return true if action succeeded else false
  def buy(asset: Asset): Boolean = {
    if (asset.ownedBy == Bank) {
      println("...Buying: " + asset.name)
      pay(Bank, asset.sellingPrice)
      asset.ownedBy = this
      true
    } else false
  }

  // pay the rent on the property if it is owned by another player. Return
  // true if action succeeded else false
  def rent(asset: Asset): Boolean = {
    if (asset.ownedBy != Bank && asset.ownedBy != this) {
      println("...Renting: " + asset.name)
      pay(asset.ownedBy, asset.rent)
      true
    } else false
  }

  // improve a property by building on it. If you own all the properties
  // in a given color group, then you can build houses and then a hotel on
  // it. Calling improve repeatedly will fire the correct transaction (or
  // if no transactions can be fired, a false is returned. Returns true or
  // false depending on whether the improve action succeeded or failed.
  def improve(buildable: PropertySlot): Boolean = {
    if (ownsColorGroup(buildable.group)) {
      if (buildable.hotels == 0) {
        if (buildable.houses < 3) {
          // build a house
          println("...Building house on: " + buildable.name)
          pay(Bank, buildable.housePrice)
          buildable.houses += 1
          true
        } else {
          // return houses, build a hotel
          println("...Building hotel on: " + buildable.name)
          Bank.pay(this, buildable.houses * buildable.housePrice)
          buildable.houses = 0
          pay(Bank, buildable.hotelPrice)
          buildable.hotels = 1
          true
        }
      } else false
    } else false
  }

  // return all properties and houses to bank at half purchase price in
  // return for cash. Return player's cash balance.'
  def liquidate(): Int = {
    println("...Liquidating assets")
    assets(asset => {true}).foreach(asset => asset match {
      case asset: PropertySlot => {
        // if there are houses/hotels, first sell that off
        Bank.pay(this, (0.5 * ((asset.houses * asset.housePrice) +
                               (asset.hotels * asset.hotelPrice))).toInt)
        asset.houses = 0
        asset.hotels = 0
        // ...followed by the property itself
        Bank.pay(this, (asset.sellingPrice * 0.5).toInt)
        asset.ownedBy = Bank
      }
      case _ => {
        Bank.pay(this, (asset.sellingPrice * 0.5).toInt)
        asset.ownedBy = Bank
      }
    })
    cash
  }

  // this method is called during an auction. Each player bids for a given
  // asset. A bid amount higher than the asset price and the player's cash
  // balance is returned. In case of a buildable asset, we attempt to model
  // player behavior by taking the maximum random bid amount over the number
  // of properties in the same color group owned by the player.
  def bid(asset: Asset): Int = {
    val randomizer = new Random()
    asset match {
      case asset: PropertySlot => {
        // check to see if player owns other buildable properties in color
        // group. Generate a random number that many times (to model player's
        // incentive to own the group) and return the max amount as the bid
        val slotsInGroup =
          buildables(buildable => {buildable.group == asset.group}).size
        val max = new Range(0, (slotsInGroup + 1), 1).
          map(i => randomizer.nextInt(cash)).
          foldLeft(0) ((max, elem) => {
            if (elem > max) elem else max
          }
        )
        asset.sellingPrice + max
      }
      case asset: Asset => {
        asset.sellingPrice + randomizer.nextInt(cash)
      }
    }
  }

  def assets(predicate: Asset => Boolean): List[Asset] = {
    Monopoly.slotsOfType[Asset](classOf[Asset]).
      filter(asset => asset.ownedBy == this).
      filter(predicate)
  }

  def buildables(predicate: PropertySlot => Boolean): 
      List[PropertySlot] = {
    Monopoly.slotsOfType[PropertySlot](classOf[PropertySlot]).
      filter(buildable => buildable.ownedBy == this).
      filter(predicate)
  }

  // method to check if the player owns the specified color group
  def ownsColorGroup(color: String): Boolean = {
    if (color == "Brown" || color == "DkBlue") {
      (buildables(buildable => { buildable.group == color }).size == 2)
    } else {
      (buildables(buildable => { buildable.group == color }).size == 3)
    }
  }

  // convenience method to check if player is in Jail (if so, he needs to
  // get out by either posting bail or using a get of out jail free card
  // if he has one.
  def inJail(): Boolean = {
    currentPosition == 10 || currentPosition == 30
  }

  // compute the repair charges due. Sum of $25/house and $100/hotel
  def computeRepairCharge(): Int = {
    val repairCharge = buildables(buildable => {true}).
      foldLeft(0) ((sum, elem) => sum + (25 * elem.houses) + 
        (100 * elem.hotels))
    repairCharge
  }

  // defines the sequence of steps that a player makes once he reaches
  // a new position. This uses certain heuristics so the Player is able
  // to play a simulated game. If the game was interactive, then the
  // player would use his judgement to call methods to control the play.
  def autoPlay(): Int = {
    if (inJail) {
      if (hasGetOutOfJailFreeCard) {
        hasGetOutOfJailFreeCard = false
      } else {
        pay(Bank, 50)
      }
    }
    val slot = Monopoly.slots(currentPosition)
    slot match {
      case slot: PropertySlot => buy(slot) || improve(slot) || rent(slot)
      case slot: RailroadSlot => buy(slot) || rent(slot)
      case slot: UtilitySlot => buy(slot) || rent(slot)
      case slot: ActionSlot => slot.execute(this)
    }
    if (cash <= 0) liquidate() else cash
  }

  // need to override hashCode and equals because we are using Player in
  // a filter equality clause
  override def hashCode(): Int = {
    name.hashCode
  }

  override def equals(that: Any): Boolean = {
    that.isInstanceOf[Player] && name == that.asInstanceOf[Player].name
  }

  override def toString(): String = {
    name + ": (pos:" + currentPosition + ",cash:" + cash + ")"
  }
}

// Models the Bank. Like a Player, the Bank can participate in financial
// transactions (pay, etc). However, the Bank is not a true player in the
// sense that it does not move across the board and own slots (except in
// the initial case where it owns everything).
object Bank extends Participant {
  var cash = 100000000

  // this method should be called if a player needs to pay all
  // the other players.
  def payOtherPlayers(source: Player, amount: Int): Unit = {
    otherPlayers(source).foreach(otherPlayer => {
      source.pay(Bank, amount)
      Bank.pay(otherPlayer, amount)
    })
  }

  // this method should be called if all the other Players should
  // pay the specified Player.
  def otherPlayersPay(target: Player, amount: Int): Unit = {
    otherPlayers(target).foreach(otherPlayer => {
      otherPlayer.pay(Bank, amount)
      Bank.pay(target, amount)
    })
  }

  // returns the player who places the maximum bid on the asset
  def auction(slot: Asset): Player = {
    println("......Holding action")
    Monopoly.players.sort((p1, p2) => p1.bid(slot) > p2.bid(slot))(0)
  }

  private def otherPlayers(thisPlayer: Player): List[Player] = {
    Monopoly.players.filter(player => player.name != thisPlayer.name)
  }
}

Here is my test. I guess I could have put this in a main() method in the Monopoly.scala file, but I like Maven's support for running JUnit tests. I wote quite a few other unit tests that I wrote as I built up the code, but I don't show them in the interests of keeping this post short.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Source: src/main/scala/com/mycompany/smi/ood/MonopolyTest.scala
package com.mycompany.smi.ood

import org.junit._
import org.junit.Assert._

class MonopolyTest {

  @Test
  def testRunSimulation(): Unit = {
    for (i <- 1 to 4) {
      Monopoly.addPlayer(new Player("Player " + i))
    }
    Monopoly.startPlay
    var turns = 1
    while (Monopoly.players.size > 1) {
      println("Turn:" + turns + ", #-players: " + Monopoly.players.size)
      for (player <- Monopoly.players) {
        println("..Player: " + player)
        player.advanceBy(player.rollDice)
        player.bankrupt = (player.autoPlay <= 0)
      }
      Monopoly.players = Monopoly.players.filter(
        player => (! player.bankrupt))
      turns = turns + 1
    }
    println("Winner: " + Monopoly.players(0))
  }
}

And here is some output from that test. As you can see, the players just get richer and richer and the simulation shows no sign of terminating, lending credence (I guess) to the truism about disciplined rule based investing strategies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Turn:1, #-players: 4
..Player: Player 1: (pos:0,cash:1500)
...Roll: (3+2)=5
...Buying: Reading Railroad
..Player: Player 4: (pos:0,cash:1500)
...Roll: (1+6)=7
...Executing: PickChanceCard
...Executing: AdvanceToStCharlesPlace
..Player: Player 2: (pos:0,cash:1500)
...Roll: (4+6)=10
...Executing: GoToJail
..Player: Player 3: (pos:0,cash:1500)
...Roll: (4+4)=8
...Buying: Vermont Avenue
...
Turn:100, #-players: 4
..Player: Player 1: (pos:32,cash:3502)
...Roll: (6+1)=7
...Renting: Boardwalk
..Player: Player 4: (pos:28,cash:2528)
...Roll: (6+1)=7
...Renting: Short Line Railroad
..Player: Player 2: (pos:31,cash:3604)
...Roll: (1+3)=4
...Renting: Short Line Railroad
..Player: Player 3: (pos:19,cash:3086)
...Roll: (1+3)=4
...Renting: Indiana Avenue
...
Turn:500, #-players: 4
..Player: Player 1: (pos:29,cash:18835)
...Roll: (6+6)=12
...Renting: Mediterenean Avenue
..Player: Player 4: (pos:23,cash:9036)
...Roll: (6+6)=12
...Renting: Short Line Railroad
..Player: Player 2: (pos:23,cash:19544)
...Roll: (6+6)=12
...Renting: Short Line Railroad
..Player: Player 3: (pos:11,cash:15171)
...Roll: (6+6)=12
...Renting: Indiana Avenue
...

If you've read my previous post, you will find that the Scala code in this post is considerably less Java-like that one, representing, I guess, a bit of evolution towards becoming more comfortable with Scala. If you are an experienced Scala programmer, you may find some of my code a bit on the verbose side - this is by design. I understand that things can be dropped in certain cases, and perhaps I will start dropping them as I gain more experience with Scala, but I find that writing out the verbose Scala idiom makes it easier to understand and more maintainable. As this Java Code Conventions document says, part of good coding is to make sure other programmers can read your code, and this will become more important as Scala gets into the mainstream, which I think may happen sooner than most people think.

As always, if you think that there are ways the code and/or design above can be improved, please let me know.

5 comments:

  1. I don't know Scala that well, but looking at the code and looking at the link to the blog post, I think your for loop needs to begin:

    for (i <- arr.indices.reverse)

    ReplyDelete
  2. Hi pitpat, thanks for the comment, but I don't see what difference it would make if I read the array backward or forward for the shuffle. Maybe I am missing something?

    ReplyDelete
  3. @pitpat: I see what you mean, you were referring to this blog link. Thanks for pointing it out, I will fix the code. Also, thinking about it some more, I think it does make a difference if one walks the array forward than backward, although I don't have the math to prove this. Because the random.nextInt() call is computed on the index, walking in reverse ensures larger variability towards the beginning of the shuffle, probably ensuring a higher quality of shuffle.

    ReplyDelete
  4. clazz.isAssignableFrom(slot.getClass)
    should be written as
    clazz.isInstance(slot)

    ReplyDelete
  5. Thank you, this is much cleaner.

    ReplyDelete

Comments are moderated to prevent spam.