summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel/Sim.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Kernel/Sim.fs')
-rw-r--r--src/LivingVillage.Kernel/Sim.fs135
1 files changed, 135 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel/Sim.fs b/src/LivingVillage.Kernel/Sim.fs
index ba3fd37..a2944e5 100644
--- a/src/LivingVillage.Kernel/Sim.fs
+++ b/src/LivingVillage.Kernel/Sim.fs
@@ -41,6 +41,9 @@ module Sim =
Honesty: float32
Greed: float32 }
+ type ItemKind =
+ | Food
+
type NpcActionKind =
| Eat
| Sleep
@@ -54,6 +57,8 @@ module Sim =
| Pay
| Hungry
| Chatted of NpcId
+ | Bought of NpcId * ItemKind * int * float32
+ | Sold of NpcId * ItemKind * int * float32
type MemoryEvent =
{ Tick: int64
@@ -62,6 +67,7 @@ module Sim =
type InteractionKind =
| ChatInit of NpcId * NpcId
+ | TradeEvent of NpcId * NpcId * ItemKind * int * float32
type InteractionEvent =
{ Tick: int64
@@ -82,6 +88,7 @@ module Sim =
type Npc =
{ Id: NpcId
Pos: Vec2
+ Inventory: Map<ItemKind, int>
Mind: Mind }
[<Struct>]
@@ -94,6 +101,24 @@ module Sim =
Npcs: Npc[]
Events: InteractionEvent list }
+ type TradeRequest =
+ { Buyer: NpcId
+ Seller: NpcId
+ Item: ItemKind
+ Quantity: int }
+
+ type TradeFailure =
+ | BuyerNotFound
+ | SellerNotFound
+ | SameParticipant
+ | InvalidQuantity
+ | OutOfStock
+ | InsufficientFunds
+
+ type TradeResult =
+ | TradeSucceeded of World
+ | TradeRejected of TradeFailure * World
+
let ticksPerSecond = 60L
let secondsPerDay = 86400L
let ticksPerDay = secondsPerDay * ticksPerSecond
@@ -159,6 +184,8 @@ module Sim =
| Pay -> 0.2f
| Hungry -> -0.4f
| Chatted _ -> chatValence
+ | Bought _ -> 0.3f
+ | Sold _ -> 0.3f
let recordMemory (tick: int64) (kind: MemoryKind) (mem: MemoryEvent list) : MemoryEvent list =
let updated = { Tick = tick; Kind = kind; Valence = valenceOfKind kind } :: mem
@@ -249,10 +276,12 @@ module Sim =
let centerY = float32 (mapHeightTiles * tilePixels / 2 - tilePixels / 2)
let personality = personalityOfSeed seed
let needs = { Hunger = 100.0f; Energy = 100.0f; Social = 100.0f; Money = 50.0f }
+ let inventory = Map.ofList [ Food, 10 ]
let startAction = decideAction (isNightTick 0L) needs personality
let npc =
{ Id = NpcId 0
Pos = { X = centerX; Y = centerY }
+ Inventory = inventory
Mind =
{ Needs = needs
Personality = personality
@@ -275,6 +304,7 @@ module Sim =
let cols = 6
let spacing = 64.0f
let needs = { Hunger = 100.0f; Energy = 100.0f; Social = 100.0f; Money = 50.0f }
+ let inventory = Map.ofList [ Food, 10 ]
let npcs =
Array.init count (fun i ->
let col = i % cols
@@ -286,6 +316,7 @@ module Sim =
let startAction = decideAction (isNightTick 0L) needs personality
{ Id = NpcId i
Pos = pos
+ Inventory = inventory
Mind =
{ Needs = needs
Personality = personality
@@ -297,6 +328,109 @@ module Sim =
Memory = [] } })
{ base_ with Npcs = npcs }
+ let private inventoryQuantity (item: ItemKind) (inventory: Map<ItemKind, int>) : int =
+ inventory |> Map.tryFind item |> Option.defaultValue 0
+
+ let private changeInventory (item: ItemKind) (delta: int) (inventory: Map<ItemKind, int>) : Map<ItemKind, int> =
+ let next = inventoryQuantity item inventory + delta
+ if next <= 0 then Map.remove item inventory else Map.add item next inventory
+
+ let private findNpcIndex (id: NpcId) (npcs: Npc[]) : int option =
+ npcs |> Array.tryFindIndex (fun npc -> npc.Id = id)
+
+ let private recentTransactionPrice (item: ItemKind) (world: World) : float32 option =
+ world.Npcs
+ |> Array.toList
+ |> List.collect (fun npc -> npc.Mind.Memory)
+ |> List.choose (fun event ->
+ match event.Kind with
+ | Bought (_, tradedItem, _, price) when tradedItem = item -> Some(event.Tick, price)
+ | Sold (_, tradedItem, _, price) when tradedItem = item -> Some(event.Tick, price)
+ | _ -> None)
+ |> List.sortByDescending fst
+ |> List.truncate 8
+ |> List.map snd
+ |> function
+ | [] -> None
+ | prices -> Some(List.average prices)
+
+ let quotePrice (request: TradeRequest) (world: World) : float32 option =
+ match findNpcIndex request.Buyer world.Npcs, findNpcIndex request.Seller world.Npcs with
+ | Some buyerIndex, Some sellerIndex when buyerIndex <> sellerIndex ->
+ let buyer = world.Npcs.[buyerIndex]
+ let seller = world.Npcs.[sellerIndex]
+ let basePrice =
+ match request.Item with
+ | Food -> 10.0f
+ let buyerUrgency = clamp ((100.0f - buyer.Mind.Needs.Hunger) / 100.0f) 0.0f 1.0f
+ let stockPressure =
+ 1.0f - clamp (float32 (inventoryQuantity request.Item seller.Inventory) / 20.0f) 0.0f 1.0f
+ let sellerNeed = clamp ((100.0f - seller.Mind.Needs.Money) / 100.0f) 0.0f 1.0f
+ let personalityFactor =
+ 0.2f * seller.Mind.Personality.Greed
+ + 0.1f * (1.0f - buyer.Mind.Personality.Honesty)
+ - 0.05f * buyer.Mind.Personality.Greed
+ let marketFactor =
+ match recentTransactionPrice request.Item world with
+ | Some recent -> clamp (0.85f + 0.15f * recent / basePrice) 0.75f 1.5f
+ | None -> 1.0f
+ let raw =
+ basePrice
+ * (0.75f
+ + 0.35f * buyerUrgency
+ + 0.30f * stockPressure
+ + 0.15f * sellerNeed
+ + personalityFactor)
+ * marketFactor
+ Some(clamp raw 0.01f 1000.0f)
+ | _ -> None
+
+ let trade (request: TradeRequest) (world: World) : TradeResult =
+ match findNpcIndex request.Buyer world.Npcs with
+ | None -> TradeRejected(BuyerNotFound, world)
+ | Some buyerIndex ->
+ match findNpcIndex request.Seller world.Npcs with
+ | None -> TradeRejected(SellerNotFound, world)
+ | Some sellerIndex when buyerIndex = sellerIndex -> TradeRejected(SameParticipant, world)
+ | Some sellerIndex when request.Quantity <= 0 -> TradeRejected(InvalidQuantity, world)
+ | Some sellerIndex ->
+ let buyer = world.Npcs.[buyerIndex]
+ let seller = world.Npcs.[sellerIndex]
+ let stock = inventoryQuantity request.Item seller.Inventory
+ if stock < request.Quantity then
+ TradeRejected(OutOfStock, world)
+ else
+ match quotePrice request world with
+ | None -> TradeRejected(SameParticipant, world)
+ | Some unitPrice ->
+ let total = unitPrice * float32 request.Quantity
+ if buyer.Mind.Needs.Money < total then
+ TradeRejected(InsufficientFunds, world)
+ else
+ let newNpcs = Array.copy world.Npcs
+ let buyerMemory =
+ recordMemory world.Tick (Bought(request.Seller, request.Item, request.Quantity, unitPrice)) buyer.Mind.Memory
+ let sellerMemory =
+ recordMemory world.Tick (Sold(request.Buyer, request.Item, request.Quantity, unitPrice)) seller.Mind.Memory
+ newNpcs.[buyerIndex] <-
+ { buyer with
+ Inventory = changeInventory request.Item request.Quantity buyer.Inventory
+ Mind =
+ { buyer.Mind with
+ Needs = { buyer.Mind.Needs with Money = buyer.Mind.Needs.Money - total }
+ Memory = buyerMemory } }
+ newNpcs.[sellerIndex] <-
+ { seller with
+ Inventory = changeInventory request.Item (-request.Quantity) seller.Inventory
+ Mind =
+ { seller.Mind with
+ Needs = { seller.Mind.Needs with Money = seller.Mind.Needs.Money + total }
+ Memory = sellerMemory } }
+ let event =
+ { Tick = world.Tick
+ Kind = TradeEvent(request.Buyer, request.Seller, request.Item, request.Quantity, unitPrice) }
+ TradeSucceeded { world with Npcs = newNpcs; Events = world.Events @ [ event ] }
+
let chatableForChat (n: Npc) : bool =
n.Mind.Action <> Sleep && n.Mind.Action <> Chat
@@ -449,6 +583,7 @@ module Sim =
for ev in queue do
match ev.Kind with
| ChatInit (a, b) -> applyChatInit ev.Tick a b newNpcs |> ignore
+ | TradeEvent _ -> ()
{ Tick = tick
Time = float tick * dtSeconds
Rng = rngNext