module Monopoly A TLA+ specification of Monopoly for N players ( N < 5). Models the standard US edition: 40 squares, 28 deeds (22 streets, 4 railroads, 2 utilities), houses/hotels, jail, GO salary, taxes, and bankruptcy. Chance and Community Chest are abstracted as non-deterministic monetary effects (the most common card outcomes). extends Naturals , Integers , FiniteSets , Sequences , TLC constants N number of players, assume N ∈ 2 . . 4 assume NAssumption ∆ = N ∈ 2 . . 4 Players ∆ = 1 . . N Board layout. Squares 0 . . 39. Categorized below. Squares ∆ = 0 . . 39 GO ∆ = 0 JustVisiting ∆ = 10 FreeParking ∆ = 20 GoToJail ∆ = 30 JailSquare ∆ = JustVisiting same square; “in jail” tracked separately ChanceSquares ∆ = { 7 , 22 , 36 } CommunityChestSquares ∆ = { 2 , 17 , 33 } TaxSquares ∆ = { 4 , 38 } Income Tax $200, Luxury Tax $100 NonPropertySquares ∆ = { GO , JustVisiting , FreeParking , GoToJail } ∪ ChanceSquares ∪ CommunityChestSquares ∪ TaxSquares Railroads ∆ = { 5 , 15 , 25 , 35 } Utilities ∆ = { 12 , 28 } Streets ∆ = Squares \ ( NonPropertySquares ∪ Railroads ∪ Utilities ) Deeds ∆ = Streets ∪ Railroads ∪ Utilities Color groups for the 22 streets. ColorGroup ∆ = [ brown 7 → { 1 , 3 } , lblue 7 → { 6 , 8 , 9 } , pink 7 → { 11 , 13 , 14 } , orange 7 → { 16 , 18 , 19 } , red 7 → { 21 , 23 , 24 } , yellow 7 → { 26 , 27 , 29 } , green 7 → { 31 , 32 , 34 } , dblue 7 → { 37 , 39 } ] ColorOf ( s ) ∆ = choose c ∈ domain ColorGroup : s ∈ ColorGroup [ c ] GroupSize ( c ) ∆ = Cardinality ( ColorGroup [ c ]) Static deed data: purchase price, base rent (no houses), house cost. Rent with houses is computed as BaseRent ∗ ( houses + 1) for simplicity. (The real game has a non-linear table; this preserves the structure without the full lookup.) Price ∆ = [ s ∈ Deeds 7 → if s ∈ Railroads then 200 else if s ∈ Utilities then 150 else if s ∈ ColorGroup brown then ( if s = 1 then 60 else 60) else if s ∈ ColorGroup lblue then ( if s = 9 then 120 else 100) else if s ∈ ColorGroup pink then ( if s = 14 then 160 else 140) else if s ∈ ColorGroup orange then ( if s = 19 then 200 else 180) 1 else if s ∈ ColorGroup red then ( if s = 24 then 240 else 220) else if s ∈ ColorGroup yellow then ( if s = 29 then 280 else 260) else if s ∈ ColorGroup green then ( if s = 34 then 320 else 300) else ( if s = 39 then 400 else 350)] BaseRent ∆ = [ s ∈ Deeds 7 → if s ∈ Railroads ∪ Utilities then 0 computed dynamically else Price [ s ] ÷ 20] HouseCost ∆ = [ s ∈ Streets 7 → if s ∈ ColorGroup brown ∪ ColorGroup lblue then 50 else if s ∈ ColorGroup pink ∪ ColorGroup orange then 100 else if s ∈ ColorGroup red ∪ ColorGroup yellow then 150 else 200] InitialMoney ∆ = 1500 GoSalary ∆ = 200 JailFine ∆ = 50 MaxJailTurns ∆ = 3 MaxHouses ∆ = 5 5 ∆ = hotel State variables. variables position , [ Players → Squares ] money , [ Players → Int ] (can be negative briefly during pay) inJail , [ Players → boolean ] jailTurns , [ Players → 0 . . MaxJailTurns ] bankrupt , [ Players → boolean ] owner , [ Deeds → Players ∪ { 0 } ] (0 ∆ = bank) houses , [ Streets → 0 . . MaxHouses ] mortgaged , [ Deeds → boolean ] current , current player whose turn it is dice , ⟨ d 1 , d 2 ⟩ last roll doublesCount , consecutive doubles this turn phase “roll” | “resolve” | “buyOrAuction” | “endTurn” vars ∆ = ⟨ position , money , inJail , jailTurns , bankrupt , owner , houses , mortgaged , current , dice , doublesCount , phase ⟩ Type invariant. TypeOK ∆ = ∧ position ∈ [ Players → Squares ] ∧ money ∈ [ Players → Int ] ∧ inJail ∈ [ Players → boolean ] ∧ jailTurns ∈ [ Players → 0 . . MaxJailTurns ] ∧ bankrupt ∈ [ Players → boolean ] ∧ owner ∈ [ Deeds → 0 . . N ] ∧ houses ∈ [ Streets → 0 . . MaxHouses ] ∧ mortgaged ∈ [ Deeds → boolean ] ∧ current ∈ Players ∧ dice ∈ (1 . . 6) × (1 . . 6) ∧ doublesCount ∈ 0 . . 3 ∧ phase ∈ { “roll” , “resolve” , “buyOrAuction” , “endTurn” } Helpers. 2 Active ∆ = { p ∈ Players : ¬ bankrupt [ p ] } NextActive ( p ) ∆ = let cand ∆ = { q ∈ Players : q > p ∧ ¬ bankrupt [ q ] } in if cand = {} then choose q ∈ Active : ∀ r ∈ Active : q ≤ r else choose q ∈ cand : ∀ r ∈ cand : q ≤ r DiceTotal ( d ) ∆ = d [1] + d [2] IsDouble ( d ) ∆ = d [1] = d [2] Owns the entire color group (for rent doubling / building). OwnsGroup ( p , s ) ∆ = ∧ s ∈ Streets ∧ ∀ t ∈ ColorGroup [ ColorOf ( s )] : owner [ t ] = p Number of railroads / utilities the owner of s holds. RailroadsOwned ( p ) ∆ = Cardinality ( { r ∈ Railroads : owner [ r ] = p } ) UtilitiesOwned ( p ) ∆ = Cardinality ( { u ∈ Utilities : owner [ u ] = p } ) Rent computation. RailroadRent ( p ) ∆ = case RailroadsOwned ( p ) = 1 → 25 2 RailroadsOwned ( p ) = 2 → 50 2 RailroadsOwned ( p ) = 3 → 100 2 RailroadsOwned ( p ) = 4 → 200 2 other → 0 UtilityRent ( p , roll ) ∆ = if UtilitiesOwned ( p ) = 2 then 10 ∗ roll else 4 ∗ roll StreetRent ( s ) ∆ = let base ∆ = BaseRent [ s ] h ∆ = houses [ s ] grp ∆ = OwnsGroup ( owner [ s ] , s ) in if h = 0 ∧ grp then 2 ∗ base else base ∗ ( h + 1) RentDue ( s , p , roll ) ∆ = if mortgaged [ s ] ∨ owner [ s ] = 0 ∨ owner [ s ] = p then 0 else if s ∈ Railroads then RailroadRent ( owner [ s ]) else if s ∈ Utilities then UtilityRent ( owner [ s ] , roll ) else StreetRent ( s ) Initial state. Init ∆ = ∧ position = [ p ∈ Players 7 → GO ] ∧ money = [ p ∈ Players 7 → InitialMoney ] ∧ inJail = [ p ∈ Players 7 → false ] ∧ jailTurns = [ p ∈ Players 7 → 0] ∧ bankrupt = [ p ∈ Players 7 → false ] ∧ owner = [ s ∈ Deeds 7 → 0] ∧ houses = [ s ∈ Streets 7 → 0] ∧ mortgaged = [ s ∈ Deeds 7 → false ] ∧ current = 1 ∧ dice = ⟨ 1 , 1 ⟩ ∧ doublesCount = 0 ∧ phase = “roll” Movement: advancing the pawn, paying GO salary on pass-through. 3 Advance ( p , k ) ∆ = let old ∆ = position [ p ] new ∆ = ( old + k )%40 pass ∆ = new < old wrapped past GO in ∧ position ′ = [ position except ! [ p ] = new ] ∧ money ′ = if pass then [ money except ! [ p ] = @ + GoSalary ] else money Pay amount from p to recipient ( recipient = 0 means bank). If p cannot pay, p is bankrupted; cash and deeds go to recipient (or revert to the bank if the creditor is the bank). Pay ( p , recipient , amount ) ∆ = if money [ p ] ≥ amount then ∧ money ′ = [ q ∈ Players 7 → if q = p then money [ q ] − amount else if recipient ∈ Players ∧ q = recipient then money [ q ] + amount else money [ q ]] ∧ unchanged ⟨ owner , houses , mortgaged , bankrupt ⟩ else ∧ bankrupt ′ = [ bankrupt except ! [ p ] = true ] ∧ money ′ = [ q ∈ Players 7 → if q = p then 0 else if recipient ∈ Players ∧ q = recipient then money [ q ] + money [ p ] else money [ q ]] ∧ owner ′ = [ s ∈ Deeds 7 → if owner [ s ] = p then ( if recipient ∈ Players then recipient else 0) else owner [ s ]] ∧ houses ′ = [ s ∈ Streets 7 → if owner [ s ] = p then 0 else houses [ s ]] ∧ mortgaged ′ = [ s ∈ Deeds 7 → if owner [ s ] = p then false else mortgaged [ s ]] Actions: a turn proceeds roll → resolve → buyOrAuction ? → endTurn Roll the dice and move (or attempt jail escape). RollAndMove ∆ = ∧ phase = “roll” ∧ ¬ bankrupt [ current ] ∧ current ′ = current ∧ ∃ d ∈ (1 . . 6) × (1 . . 6) : ∧ dice ′ = d ∧ if inJail [ current ] then In jail: doubles get out free; otherwise pay $50 after 3 turns. if IsDouble ( d ) then ∧ inJail ′ = [ inJail except ! [ current ] = false ] ∧ jailTurns ′ = [ jailTurns except ! [ current ] = 0] ∧ Advance ( current , DiceTotal ( d )) ∧ doublesCount ′ = 0 no extra turn from jail-doubles ∧ phase ′ = “resolve” ∧ unchanged ⟨ bankrupt , owner , houses , mortgaged ⟩ else if jailTurns [ current ] = MaxJailTurns − 1 then must pay fine, exit jail, and advance let old ∆ = position [ current ] new ∆ = ( old + DiceTotal ( d ))%40 pass ∆ = new < old gain ∆ = ( if pass then GoSalary else 0) 4 − JailFine in ∧ money [ current ] + gain ≥ 0 ∧ money ′ = [ money except ! [ current ] = @ + gain ] ∧ position ′ = [ position except ! [ current ] = new ] ∧ inJail ′ = [ inJail except ! [ current ] = false ] ∧ jailTurns ′ = [ jailTurns except ! [ current ] = 0] ∧ doublesCount ′ = 0 ∧ phase ′ = “resolve” ∧ unchanged ⟨ bankrupt , owner , houses , mortgaged ⟩ else ∧ jailTurns ′ = [ jailTurns except ! [ current ] = @ + 1] ∧ phase ′ = “endTurn” ∧ doublesCount ′ = 0 ∧ unchanged ⟨ position , money , inJail , bankrupt , owner , houses , mortgaged ⟩ else Free play. if IsDouble ( d ) ∧ doublesCount = 2 then third double in a row → Go to Jail ∧ position ′ = [ position except ! [ current ] = JailSquare ] ∧ inJail ′ = [ inJail except ! [ current ] = true ] ∧ jailTurns ′ = [ jailTurns except ! [ current ] = 0] ∧ doublesCount ′ = 0 ∧ phase ′ = “endTurn” ∧ unchanged ⟨ money , bankrupt , owner , houses , mortgaged ⟩ else ∧ Advance ( current , DiceTotal ( d )) ∧ doublesCount ′ = if IsDouble ( d ) then doublesCount + 1 else 0 ∧ phase ′ = “resolve” ∧ unchanged ⟨ inJail , jailTurns , bankrupt , owner , houses , mortgaged ⟩ Resolve the square the player landed on. ResolveSquare ∆ = ∧ phase = “resolve” ∧ current ′ = current ∧ let p ∆ = current s ∆ = position [ current ] r ∆ = DiceTotal ( dice ) in case s = GoToJail → ∧ position ′ = [ position except ! [ p ] = JailSquare ] ∧ inJail ′ = [ inJail except ! [ p ] = true ] ∧ jailTurns ′ = [ jailTurns except ! [ p ] = 0] ∧ doublesCount ′ = 0 ∧ phase ′ = “endTurn” ∧ unchanged ⟨ money , bankrupt , owner , houses , mortgaged , dice ⟩ 2 s = 4 → Income Tax ∧ Pay ( p , 0 , 200) ∧ phase ′ = “endTurn” ∧ unchanged ⟨ position , inJail , jailTurns , doublesCount , dice ⟩ 2 s = 38 → Luxury Tax ∧ Pay ( p , 0 , 100) ∧ phase ′ = “endTurn” ∧ unchanged ⟨ position , inJail , jailTurns , doublesCount , dice ⟩ 2 s ∈ ChanceSquares ∪ CommunityChestSquares → Abstracted: nondeterministic +/- in a small range. 5 ∧ ∃ delta ∈ { − 100 , − 50 , 0 , 50 , 100 , 200 } : if delta < 0 then Pay ( p , 0 , − delta ) else ∧ money ′ = [ money except ! [ p ] = @ + delta ] ∧ unchanged ⟨ owner , houses , mortgaged , bankrupt ⟩ ∧ phase ′ = “endTurn” ∧ unchanged ⟨ position , inJail , jailTurns , doublesCount , dice ⟩ 2 s ∈ Deeds → if owner [ s ] = 0 then ∧ phase ′ = “buyOrAuction” ∧ unchanged ⟨ position , money , inJail , jailTurns , bankrupt , owner , houses , mortgaged , doublesCount , dice ⟩ else pay rent (or nothing if owner = p , mortgaged , etc.) ∧ Pay ( p , owner [ s ] , RentDue ( s , p , r )) ∧ phase ′ = “endTurn” ∧ unchanged ⟨ position , inJail , jailTurns , doublesCount , dice ⟩ 2 other → GO , JustVisiting , FreeParking ∧ phase ′ = “endTurn” ∧ unchanged ⟨ position , money , inJail , jailTurns , bankrupt , owner , houses , mortgaged , doublesCount , dice ⟩ Buy the unowned deed you landed on. BuyProperty ∆ = ∧ phase = “buyOrAuction” ∧ let p ∆ = current s ∆ = position [ p ] in ∧ s ∈ Deeds ∧ owner [ s ] = 0 ∧ money [ p ] ≥ Price [ s ] ∧ money ′ = [ money except ! [ p ] = @ − Price [ s ]] ∧ owner ′ = [ owner except ! [ s ] = p ] ∧ phase ′ = “endTurn” ∧ unchanged ⟨ position , inJail , jailTurns , bankrupt , houses , mortgaged , current , doublesCount , dice ⟩ Decline to buy: property stays with bank (auction abstracted out). DeclineProperty ∆ = ∧ phase = “buyOrAuction” ∧ phase ′ = “endTurn” ∧ unchanged ⟨ position , money , inJail , jailTurns , bankrupt , owner , houses , mortgaged , current , doublesCount , dice ⟩ Build a house on a fully-owned color group (between turns). BuildHouse ∆ = ∧ phase = “endTurn” ∧ ∃ s ∈ Streets : ∧ owner [ s ] = current ∧ OwnsGroup ( current , s ) ∧ ¬ mortgaged [ s ] ∧ houses [ s ] < MaxHouses even-build rule: cannot be more than 1 ahead of any group-mate ∧ ∀ t ∈ ColorGroup [ ColorOf ( s )] : houses [ s ] ≤ houses [ t ] ∧ money [ current ] ≥ HouseCost [ s ] ∧ money ′ = [ money except ! [ current ] = @ − HouseCost [ s ]] 6 ∧ houses ′ = [ houses except ! [ s ] = @ + 1] ∧ unchanged ⟨ position , inJail , jailTurns , bankrupt , owner , mortgaged , current , doublesCount , dice , phase ⟩ Mortgage / unmortgage a deed (between turns). Mortgage ∆ = ∧ phase = “endTurn” ∧ ∃ s ∈ Deeds : ∧ owner [ s ] = current ∧ ¬ mortgaged [ s ] ∧ ( if s ∈ Streets then houses [ s ] = 0 else true ) ∧ money ′ = [ money except ! [ current ] = @ + ( Price [ s ] ÷ 2)] ∧ mortgaged ′ = [ mortgaged except ! [ s ] = true ] ∧ unchanged ⟨ position , inJail , jailTurns , bankrupt , owner , houses , current , doublesCount , dice , phase ⟩ Unmortgage ∆ = ∧ phase = “endTurn” ∧ ∃ s ∈ Deeds : let back ∆ = ( Price [ s ] ÷ 2) + ( Price [ s ] ÷ 20) + 10% interest in ∧ owner [ s ] = current ∧ mortgaged [ s ] ∧ money [ current ] ≥ back ∧ money ′ = [ money except ! [ current ] = @ − back ] ∧ mortgaged ′ = [ mortgaged except ! [ s ] = false ] ∧ unchanged ⟨ position , inJail , jailTurns , bankrupt , owner , houses , current , doublesCount , dice , phase ⟩ End the turn (or take an extra turn on doubles). EndTurn ∆ = ∧ phase = “endTurn” ∧ Cardinality ( Active ) > 1 ∧ phase ′ = “roll” ∧ if IsDouble ( dice ) ∧ doublesCount > 0 ∧ ¬ inJail [ current ] then ∧ current ′ = current ∧ doublesCount ′ = doublesCount else ∧ current ′ = NextActive ( current ) ∧ doublesCount ′ = 0 ∧ unchanged ⟨ position , money , inJail , jailTurns , bankrupt , owner , houses , mortgaged , dice ⟩ Next-state and spec. GameOver ∆ = Cardinality ( Active ) ≤ 1 Next ∆ = if GameOver then unchanged vars else ∨ RollAndMove ∨ ResolveSquare ∨ BuyProperty ∨ DeclineProperty ∨ BuildHouse ∨ Mortgage ∨ Unmortgage ∨ EndTurn Spec ∆ = Init ∧ 2 [ Next ] vars ∧ WF vars ( EndTurn ) Invariants and properties. 7 Houses on a color group never differ by more than one (even-build rule). EvenBuilding ∆ = ∀ c ∈ domain ColorGroup : ∀ s , t ∈ ColorGroup [ c ] : owner [ s ] = owner [ t ] ∧ owner [ s ] ̸ = 0 ⇒ ( houses [ s ] − houses [ t ] ∈ − 1 . . 1) A bankrupt player owns nothing and has zero cash. BankruptcyClean ∆ = ∀ p ∈ Players : bankrupt [ p ] ⇒ ∧ money [ p ] = 0 ∧ ∀ s ∈ Deeds : owner [ s ] ̸ = p No deed is owned by a bankrupt player. OwnershipValid ∆ = ∀ s ∈ Deeds : owner [ s ] ̸ = 0 ⇒ ¬ bankrupt [ owner [ s ]] The current player is always an active player while the game continues. TurnValid ∆ = Cardinality ( Active ) > 1 ⇒ ¬ bankrupt [ current ] Liveness: someone eventually wins (only one active player remains). EventuallyOneWinner ∆ = 3 ( Cardinality ( Active ) ≤ 1) 8