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.