Changes
1 changed files (+218/-138)
-
-
@@ -101,7 +101,15 @@ inductive BuildingVariant| office | shop | factory deriving Inhabited, BEq, Lean.ToJson, Lean.FromJson deriving Inhabited, BEq, Repr, Lean.ToJson, Lean.FromJson instance : ToString BuildingVariant where toString | .house => "house" | .apartment => "apartment" | .office => "office" | .shop => "shop" | .factory => "factory" def BuildingVariant.ofString? : String → Option BuildingVariant | "h" | "house" => some .house
-
@@ -170,6 +178,7 @@ structure Peep wherepos : Nat3 dest : Nat dir : Option Nat brokenDown : Bool deriving BEq, Lean.ToJson, Lean.FromJson /-- Grid helper functions -/
-
@@ -199,7 +208,10 @@ structure TrafficLight whereredLen : Nat greenLen : Nat shift : Nat deriving Lean.ToJson, Lean.FromJson deriving BEq, Lean.ToJson, Lean.FromJson instance : ToString TrafficLight where toString t := s!"Red: {t.redLen}, Green: {t.greenLen}, Shift: {t.shift}" def TrafficLight.isRed (t : TrafficLight) (ticks : Nat) : Bool := (ticks / ticksPerSecond - t.shift) % (t.redLen + t.greenLen) < t.redLen
-
@@ -228,10 +240,25 @@ instance [Lean.FromJson α] : Lean.FromJson (Vector α n) whereelse throw s!"expected size {n}, got {A.size}" inductive Control | none | yield | trafficLight (t : TrafficLight) deriving Inhabited, BEq, Lean.ToJson, Lean.FromJson instance : ToString Control where toString | .none => "" | .yield => "Yield" | .trafficLight t => toString t def Control.isRed (ticks : Nat) | trafficLight t => t.isRed ticks | _ => false structure Point where name : String isYield : Bool trafficLight : Option TrafficLight control : Control e : Vector Road 27 deriving Inhabited, Lean.ToJson, Lean.FromJson
-
@@ -373,7 +400,7 @@ def doTick : StateM State Unit := dolet origPeeps := let g := (← get).grid (← (← get).peeps.shuffle).partition (fun p ↦ if h : g.contains p.pos then !g[p.pos].isYield else true) if h : g.contains p.pos then g[p.pos].control != .yield else true) for p in origPeeps.1 ++ origPeeps.2 do let u := p.pos let s := ← get
-
@@ -405,10 +432,13 @@ def doTick : StateM State Unit := doif p'.dest != p.dest then p' := { p' with pos := s.buildings[p.dest]!.exit } peeps := peeps.push p' else if (if h : s.grid.contains u then (if h : s.grid[u].trafficLight.isSome then s.grid[u].trafficLight.get h |>.isRed s.ticks else false) else false) then else if (if h : s.grid.contains u then s.grid[u].control.isRed s.ticks else false) then -- Traffic light occupied := occupied.insert u peeps := peeps.push ({ p with dir := none }) else if p.brokenDown then occupied := occupied.insert u peeps := peeps.push ({ p with dir := none, brokenDown := (← rand 100) != 0 }) else let dist := s.dists[p.dest]! let mut neighbors := #[]
-
@@ -432,11 +462,11 @@ def doTick : StateM State Unit := do| _ => pure () neighbors := neighbors.qsort (fun a b ↦ a.1 < b.1 || (a.1 == b.1 && a.2 < b.2)) let mut moved := false let mut p' := p for (d, i) in neighbors do -- if d ≥ min (neighbors[0]!.1 + 100) (2 * neighbors[0]!.1 + 5) then -- Don't move if this makes us take a really long detour -- TODO better heuristic -- TODO: better heuristic -- break let v := u.appd i if i < 27 then
-
@@ -444,7 +474,7 @@ def doTick : StateM State Unit := docontinue occupied := occupied.insert v occupiedMid := occupiedMid.insert (u + v) peeps := peeps.push { p with pos := v, dir := some i } p' := { p with pos := v, dir := some i } else let v':= u.appdk i 2 if s.occupied.contains v || occupied.contains v || occupiedMid.contains (u + v) || s.occupied.contains v' || occupied.contains v' || occupiedMid.contains (v + v') then
-
@@ -453,12 +483,12 @@ def doTick : StateM State Unit := dooccupiedMid := occupiedMid.insert (u + v) occupied := occupied.insert v' occupiedMid := occupiedMid.insert (v + v') peeps := peeps.push { p with pos := v', dir := some i } moved := true p' := { p with pos := v', dir := some i } break if !moved then if p' == p then occupied := occupied.insert u peeps := peeps.push { p with pos := u, dir := none } p' := { p with pos := u, dir := none } peeps := peeps.push { p' with brokenDown := (← rand 5000) != 0 } setf peeps peeps setf occupied occupied modifyf ticks (· + 1)
-
@@ -497,37 +527,36 @@ def spend (cost : Nat) : StateT State IO Unit := dothrow <| .userError s!"Need ${cost}, only have ${(← get).money}" modifyf money (· - cost) /-- Counterclockwise thingy -/ def sideToPos (pos size : Nat3) (side : Nat × Nat) := pos + (match side.1 with | 0 => ⟨side.2, 0, 0⟩ | 1 => ⟨size.x, 0, side.2⟩ | 2 => ⟨size.x - side.2, 0, size.z⟩ | _ => ⟨0, 0, size.z - side.2⟩ : Nat3) /-- Add a building to the state -/ def addBuilding (variant : BuildingVariant) (pos size : Nat3) (entrance exit : Nat × Nat) : StateT State IO Unit := do def addBuilding (variant : BuildingVariant) (pos size entrance exit : Nat3) : StateT State IO Unit := do if entrance == exit then throw <| .userError "Building entrance cannot be in same position as exit" let notOnSide (p : Nat3) := p.x != pos.x && p.x != pos.x + size.x && p.z != pos.z && p.z != pos.z + size.z if notOnSide entrance || notOnSide exit then throw <| .userError "Building entrance or exit not on side of building" for x in [pos.x:pos.x + size.x + 1] do for y in [pos.y:pos.y + size.y + 1] do for z in [pos.z:pos.z + size.z + 1] do if (← get).grid.contains ⟨x, y, z⟩ then throw <| IO.userError "Building collides with existing road" for b' in (← get).buildings do if Building.checkCollide pos size b'.pos b'.size then throw <| IO.userError "Building collides with existing building" if Building.checkCollide (entrance - ⟨1, 0, 1⟩) ⟨2, 1, 2⟩ b'.pos b'.size then throw <| IO.userError "Building entrance collides with existing building" if Building.checkCollide (exit - ⟨1, 0, 1⟩) ⟨2, 1, 2⟩ b'.pos b'.size then throw <| IO.userError "Building exit collides with existing building" let b : Building := { variant pos size entrance := sideToPos pos size entrance exit := sideToPos pos size exit entrance exit spots := Building.capacity size variant deleted := false } spend b.cost for x in [b.pos.x:b.pos.x + b.size.x + 1] do for y in [b.pos.y:b.pos.y + b.size.y + 1] do for z in [b.pos.z:b.pos.z + b.size.z + 1] do if (← get).grid.contains ⟨x, y, z⟩ then throw <| IO.userError "Building collides with existing road" for b' in (← get).buildings do if Building.checkCollide b.pos b.size b'.pos b'.size then throw <| IO.userError "Building collides with existing building" -- Randomly iterate through empty spots in buildings of the opposite kind and create new peeps let mut spots := b.spots while spots > 0 do
-
@@ -542,6 +571,7 @@ def addBuilding (variant : BuildingVariant) (pos size : Nat3) (entrance exit : Npos := b.exit dest := idx dir := none brokenDown := false }) else let newIdx := (← get).buildings.size
-
@@ -551,6 +581,7 @@ def addBuilding (variant : BuildingVariant) (pos size : Nat3) (entrance exit : Npos := (← get).buildings[idx]!.exit dest := newIdx dir := none brokenDown := false }) spots := spots - 1 modifyf buildings (·.modify idx (fun b ↦ { b with spots := b.spots - 1 }))
-
@@ -594,10 +625,10 @@ def addRoad (start stop : Nat3) (isHigh : Bool) : StateT State IO Unit := dothrow <| .userError "Road cannot go straight up or down" let road : Road := if isHigh then .high else .low spend <| length * road.cost dir start.y -- TODO: Better heuristic here let mut name := "" -- Subtract 1 so that we need at least one overlap to share the same street name let stop := start.appdk dir (length - 1) for u in [start, { start with x := start.x + 1 }, { start with z := start.z + 1 }, { start with x := start.x - 1 }, { start with z := start.z - 1 }, stop] do let stop := start.appdk dir length for u in [start, stop, { start with x := start.x + 1 }, { start with z := start.z + 1 }, { start with x := start.x - 1 }, { start with z := start.z - 1 }] do if (← get).grid.contains u then -- Yeah this is not ideal but Lean doesn't know the two `← get`s are the same name := (← get).grid[u]!.name
-
@@ -612,7 +643,7 @@ def addRoad (start stop : Nat3) (isHigh : Bool) : StateT State IO Unit := domodifyf grid fun g ↦ Id.run do let mut g := g if !g.contains v then g := g.insert v ⟨name, false, none, .replicate 27 .none⟩ g := g.insert v ⟨name, .none, .replicate 27 .none⟩ g.modify v (fun p ↦ if i < length then { p with e := p.e.set! dir road } else p) mkDists
-
@@ -621,13 +652,13 @@ def addYield (pos : Nat3) : StateT State IO Unit := dospend 10 if !(← get).grid.contains pos then throw <| .userError s!"Could not place yield at {pos}" modifyf grid (·.modify pos fun p ↦ { p with isYield := true }) modifyf grid (·.modify pos fun p ↦ { p with control := .yield }) def addTrafficLight (pos : Nat3) (t : TrafficLight) : StateT State IO Unit := do spend 100 if !(← get).grid.contains pos then throw <| .userError s!"Could not place traffic light at {pos}" modifyf grid (·.modify pos fun p ↦ { p with trafficLight := some t }) modifyf grid (·.modify pos fun p ↦ { p with control := .trafficLight t }) def addIntersection (pos : Nat3) : StateT State IO Unit := do -- Main roads
-
@@ -671,10 +702,10 @@ def handleCmd (cmd : String) : StateT State IO Unit := dodelete ⟨dims[0], dims[1], dims[2]⟩ ⟨dims[3], dims[4], dims[5]⟩ | "b" :: variant :: dims => let variant := BuildingVariant.ofString? variant if h : dims.length = 10 && variant.isSome then if h : dims.length = 12 && variant.isSome then let dims := dims.map String.toNat! have : dims.length = 10 := by grind addBuilding (variant.get (by grind)) ⟨dims[0], dims[1], dims[2]⟩ ⟨dims[3], dims[4], dims[5]⟩ (dims[6], dims[7]) (dims[8], dims[9]) have : dims.length = 12 := by grind addBuilding (variant.get (by grind)) ⟨dims[0], dims[1], dims[2]⟩ ⟨dims[3], dims[4], dims[5]⟩ ⟨dims[6], dims[7], dims[8]⟩ ⟨dims[9], dims[10], dims[11]⟩ else throw <| .userError "Failed to parse add building command" | "r" :: dims =>
-
@@ -726,8 +757,8 @@ def render (s : State) (camera : Camera3D) (frames : Nat) : IO Unit := doif !b.deleted then let sizeV3 := b.size.toVector3 let posV3 := b.pos.toVector3Shift s + sizeV3 / 2.0 let entranceV3 := b.entrance.toVector3Shift s + (Vector3.mk 0 0.05 0) let exitV3 := b.exit.toVector3Shift s + (Vector3.mk 0 0.05 0) let entranceV3 := b.entrance.toVector3Shift s + ⟨0, 0.05, 0⟩ let exitV3 := b.exit.toVector3Shift s + ⟨0, 0.05, 0⟩ drawCubeV posV3 sizeV3 b.color drawCubeWiresV posV3 sizeV3 .black drawCubeV entranceV3 ⟨0.1, 0.1, 0.1⟩ .green
-
@@ -743,7 +774,7 @@ def render (s : State) (camera : Camera3D) (frames : Nat) : IO Unit := dolet pos2D ← getWorldToScreen (posV3 + ⟨0, 0.2, 0⟩) camera let address := if b.entrance.x == b.pos.x || b.entrance.x == b.pos.x + b.size.x then b.entrance.z else b.entrance.x drawText s!"{address} {name}" pos2D.x.toUInt64.toNat pos2D.y.toUInt64.toNat 10 Color.black drawText s!"{address} {name} ({b.variant})" pos2D.x.toUInt64.toNat pos2D.y.toUInt64.toNat 10 Color.black beginMode3D camera -- Render roads for (pos, pt) in s.grid do
-
@@ -764,11 +795,13 @@ def render (s : State) (camera : Camera3D) (frames : Nat) : IO Unit := do-- Fake shadows if pos.y != s.origin.y || stop.y != s.origin.y then drawLine3D { posV3 with y := 0 } { stopV3 with y := 0 } Color.Raylean.gray if pt.isYield then match pt.control with | .yield => drawCubeV (posV3 + ⟨0, 0.075, 0⟩) ⟨0.02, 0.02, 0.02⟩ .yellow if h : pt.trafficLight.isSome then let t := pt.trafficLight.get h | .trafficLight t => drawCubeV (posV3 + ⟨0, 0.075, 0⟩) ⟨0.02, 0.02, 0.02⟩ (if t.isRed s.ticks then .red else .green) | .none => pure () -- Render peeps for peep in s.peeps do -- Unfortunately we can't use `Nat3.toVector3` here because they're `Int`s
-
@@ -823,13 +856,6 @@ def getMouse3D (y : Int) (camera : Camera3D) : IO Vector3 := do-- `.dot` is just element-wise product, not the dot product! return project (pos2.dot windowScale) camera (Float.ofInt (y / sensitivity) / scale) (← getScreenWidth).toFloat (← getScreenHeight).toFloat def getInput (stdin : IO.FS.Stream) := do IO.print "> " return (← stdin.getLine).trimAsciiEnd.toString def padTime (n : Nat) := if n < 10 then s!"0{n}" else s!"{n}" def spawnInitBuildings : StateT State IO Unit := do let randPos : StateT State IO Nat3 := do return ⟨(← get).origin.x - 50 + (← rand 100), 10, (← get).origin.z - 50 + (← rand 100)⟩
-
@@ -840,37 +866,134 @@ def spawnInitBuildings : StateT State IO Unit := doif 50 < x && x < 150 && 50 < z && z < 150 then continue return ⟨(← get).origin.x - 100 + x, 10, (← get).origin.z - 100 + z⟩ -- Spawn the commerical buildings first so peeps' workplaces get evenly distributed among them for i in [:3] do let sideToPos (pos size : Nat3) (side : Fin 4) (offset : Nat) := pos + (match side with | 0 => ⟨offset, 0, 0⟩ | 1 => ⟨size.x, 0, offset⟩ | 2 => ⟨size.x - offset, 0, size.z⟩ | 3 => ⟨0, 0, size.z - offset⟩) let randBuilding (variant : BuildingVariant) (size : Nat3) (offset : Nat) (isClose : Bool) : StateT State IO Unit := do let pos ← (if isClose then randPos else randPosExt) let size := if (← rand 2) == 0 then size else ⟨size.z, size.y, size.x⟩ let side ← rand 4 try addBuilding .office (← randPos) ⟨10, 2, 5⟩ (side, 2) (side, 3) addBuilding variant pos size (sideToPos pos size side offset) (sideToPos pos size side (offset + 1)) catch e => IO.println e -- Spawn the commerical buildings first so peeps' workplaces get evenly distributed among them for i in [:3] do randBuilding .office ⟨10, 2, 5⟩ 3 true for i in [:10] do let side ← rand 4 try addBuilding .shop (← randPos) ⟨5, 1, 10⟩ (side, 3) (side, 4) catch e => IO.println e randBuilding .shop ⟨10, 1, 5⟩ 3 true for i in [:5] do let side ← rand 4 try addBuilding .factory (← randPosExt) ⟨10, 5, 10⟩ (side, 5) (side, 6) catch e => IO.println e for i in [:100] do let side ← rand 4 try addBuilding .house (← randPosExt) ⟨5, 1, 5⟩ (side, 2) (side, 3) catch e => IO.println e randBuilding .factory ⟨10, 5, 10⟩ 5 false for i in [:50] do randBuilding .house ⟨5, 1, 3⟩ 1 false for i in [:10] do let side ← rand 4 try addBuilding .apartment (← randPos) ⟨10, 3, 5⟩ (side, 1) (side, 2) catch e => IO.println e randBuilding .apartment ⟨10, 3, 5⟩ 1 true inductive Action | delete | road (isHigh : Bool) | multiroad (isHigh : Bool) | building (variant : BuildingVariant) | yield | trafficLight (phase : Bool) | intersection instance : ToString Action where toString | .delete => "Delete" | .road isHigh => s!"Build {if isHigh then "highway" else "road"}" | .multiroad isHigh => s!"Build multilane {if isHigh then "highway" else "road"}" | .building variant => s!"Build {variant}" | .yield => "Build yield" | .trafficLight phase => s!"Build traffic light (phase {if phase then 2 else 1})" | .intersection => "Build intersection" def keyAction : List (Char × Action) := [ ('0', .delete), ('1', .road false), ('2', .road true), ('3', .multiroad false), ('4', .multiroad true), ('5', .building .house), ('6', .building .apartment), ('7', .building .office), ('8', .building .shop), ('9', .building .factory), ('Z', .yield), ('X', .trafficLight false), ('C', .trafficLight true), ('V', .intersection), ] def endpointsToPosSize (pos pos' : Nat3) : Nat3 × Nat3 := (⟨min pos.x pos'.x, min pos.y pos'.y, min pos.z pos'.z⟩, ⟨diff pos.x pos'.x, diff pos.y pos'.y, diff pos.z pos'.z⟩) def doAction (actionState : List Nat3) (curAction : Action) (mousePos : Nat3) (newHeight : Nat) : StateT State IO (List Nat3) := do match actionState, curAction with | pos :: _, .delete => delete pos mousePos | pos :: _, .road isHigh => addRoad pos mousePos isHigh | pos :: _, .multiroad isHigh => -- TODO addRoad pos mousePos isHigh | pos :: pos' :: entrance :: _, .building variant => let (pos, size) := endpointsToPosSize pos { pos' with y := pos.y + newHeight / sensitivity } addBuilding variant pos size entrance mousePos | _, .yield => addYield mousePos | _, .trafficLight phase => addTrafficLight mousePos ⟨10, 10, if phase then 10 else 0⟩ | _, _ => return actionState ++ [mousePos] return [] def renderAction (actionState : List Nat3) (curAction : Action) (mousePos : Nat3) (s : State) (newHeight : Nat) : StateT State IO Unit := do let mousePosV3 := mousePos.toVector3Shift s match actionState, curAction with | pos :: _, .delete => let posV3 := pos.toVector3Shift s drawCubeV ((posV3 + mousePosV3) / 2.0) (mousePosV3 - posV3) Color.Raylean.maroon | pos :: _, .road isHigh => let (length, dir) := endpointsToRoad pos mousePos let stop := pos.appdk dir length let posV3 := pos.toVector3Shift s let stopV3 := stop.toVector3Shift s drawLine3D posV3 stopV3 (if isHigh then Color.Raylean.pink else Color.Raylean.gold) -- Fake shadows if pos.y != s.origin.y || stop.y != s.origin.y then drawLine3D { posV3 with y := 0 } { stopV3 with y := 0 } Color.Raylean.gray | pos :: _, .multiroad isHigh => -- TODO pure () | poses, .building _ => let poses := poses ++ [mousePos] if h : poses.length > 1 then let (pos, size) := endpointsToPosSize poses[0] { poses[1] with y := poses[0].y + newHeight / sensitivity } let posV3 := pos.toVector3Shift s drawCubeV (posV3 + size.toVector3 / 2.0) size.toVector3 Color.Raylean.lightgray drawCubeWiresV (posV3 + size.toVector3 / 2.0) size.toVector3 .black if h : poses.length > 2 then drawCubeV (poses[2].toVector3Shift s + ⟨0, 0.05, 0⟩) ⟨0.1, 0.1, 0.1⟩ .green if h : poses.length > 3 then drawCubeV (poses[3].toVector3Shift s + ⟨0, 0.05, 0⟩) ⟨0.1, 0.1, 0.1⟩ .red | _, .yield | _, .trafficLight _ => drawCubeV (mousePosV3 + ⟨0, 0.075, 0⟩) ⟨0.02, 0.02, 0.02⟩ Color.Raylean.lime | _, .intersection => drawCubeV (mousePosV3 + ⟨0.25, 0, 0.25⟩) ⟨0.5, 0, 0.5⟩ Color.Raylean.gray | _, _ => pure () def getInput (stdin : IO.FS.Stream) := do IO.print "> " return (← stdin.getLine).trimAsciiEnd.toString def padTime (n : Nat) := if n < 10 then s!"0{n}" else s!"{n}" def gameLoop : StateT State IO Unit := do spawnInitBuildings
-
@@ -884,9 +1007,10 @@ def gameLoop : StateT State IO Unit := do} let stdin ← IO.getStdin let mut task ← IO.asTask <| getInput stdin let mut start : Option Nat3 := none let mut curAction := 1 let mut actionState : List Nat3 := [] let mut curAction := .road false let mut y : Int := 0 let mut newHeight : Nat := sensitivity let mut frames := 0 while !(← windowShouldClose) do -- Fix `up` to prevent the Q and E keys from messing it up
-
@@ -898,37 +1022,22 @@ def gameLoop : StateT State IO Unit := doif (← isMouseButtonPressed MouseButton.left) then do let mousePos := (← getMouse3D y camera).toNat3Shift (← get) try match start, curAction with | _, 3 => addYield mousePos | _, 4 => addTrafficLight mousePos ⟨10, 10, 0⟩ | _, 5 => addTrafficLight mousePos ⟨10, 10, 10⟩ | _, 6 => addIntersection mousePos | none, _ => start := some mousePos | some pos, 1 => addRoad pos mousePos false start := none | some pos, 2 => addRoad pos mousePos true start := none | some pos, _ => delete pos mousePos start := none actionState ← doAction actionState curAction mousePos newHeight catch e => IO.println e if (← isMouseButtonPressed MouseButton.right) then do start := none actionState := [] if (← isKeyDown Key.left) then do modifyf speed (· - 1) if (← isKeyDown Key.right) then do modifyf speed (· + 1) for i in [:10] do if (← isKeyDown <| '0'.toNat + i) then do curAction := i if (← isKeyDown '-'.toNat) then do newHeight := newHeight - 1 if (← isKeyDown '='.toNat) then do newHeight := newHeight + 1 for (key, action) in keyAction do if (← isKeyDown key.toNat) then do curAction := action if ← IO.hasFinished task then let cmd ← (.ofExcept task.get) try
-
@@ -946,47 +1055,18 @@ def gameLoop : StateT State IO Unit := doclearBackground Color.white let mousePosV3 ← getMouse3D y camera let mousePos := mousePosV3.toNat3Shift s let mousePosV3Snap := mousePos.toVector3Shift s renderWithCamera camera do render s camera frames drawGrid (s.origin.x / scaleN * 2) 1 match start, curAction with | _, 3 | _, 4 | _, 5 => drawCubeV (mousePosV3Snap + ⟨0, 0.075, 0⟩) ⟨0.02, 0.02, 0.02⟩ Color.Raylean.lime | _, 6 => drawCubeV (mousePosV3Snap + ⟨0.25, 0, 0.25⟩) ⟨0.5, 0, 0.5⟩ Color.Raylean.gray | none, _ => pure () | some pos, 1 | some pos, 2 => let (length, dir) := endpointsToRoad pos mousePos let stop := pos.appdk dir length let posV3 := pos.toVector3Shift s let stopV3 := stop.toVector3Shift s drawLine3D posV3 stopV3 (if curAction == 1 then Color.Raylean.pink else Color.Raylean.gold) -- Fake shadows if pos.y != s.origin.y || stop.y != s.origin.y then drawLine3D { posV3 with y := 0 } { stopV3 with y := 0 } Color.Raylean.gray | some pos, _ => let posV3 := pos.toVector3Shift s drawCubeV ((posV3 + mousePosV3) / 2.0) (mousePosV3 - posV3) Color.Raylean.maroon -- TODO bind more numbers for building buildings let actionText := match curAction with | 1 => "Build road" | 2 => "Build highway" | 3 => "Build yield" | 4 => "Build traffic light (phase 1)" | 5 => "Build traffic light (phase 2)" | 6 => "Build simple intersection" | _ => "Delete" drawText s!"Active control: {actionText}" 10 10 20 .black renderAction actionState curAction mousePos s newHeight drawText s!"Active action: {curAction}" 10 10 20 .black drawText s!"Time: {s.ticks / ticksPerSecond / 60 / 60}:{padTime <| s.ticks / ticksPerSecond / 60 % 60}:{padTime <| s.ticks / ticksPerSecond % 60}" 10 40 20 .black drawText s!"Money: ${s.money}" 10 70 20 .black drawText s!"Population: {s.peeps.size}" 10 100 20 .black drawText s!"Speed: {s.speed / sensitivity}" 10 130 20 .black drawText s!"{mousePos}" 10 160 20 .black if s.grid.contains mousePos then drawText s!"{s.grid[mousePos]!.name}" 10 190 20 .black if hs : s.grid.contains mousePos then drawText s!"{s.grid[mousePos].name} {s.grid[mousePos].control}" 10 190 20 .black drawFPS ((← getScreenWidth) - 100) 10 closeWindow
-