//! The economy's actors (workplaces, markets and carts), the jobs people hold, and how prices //! are set and haggled over. //! //! Goods only enter the economy when people gather or craft them and only leave it when they //! are eaten or used up. Coins never appear or vanish: wages, purchases and sales move them //! between people, workplaces and markets. use super::actors::{PersonMsg, ReplyBody, RequestId, reply}; use super::router::{EntityKey, Kind, Router}; use picard::actors::{Actor, ActorFactoryArgs, Context as ActorContext, Sender}; use serde::{Deserialize, Serialize}; use std::sync::Arc; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub(super) struct Goods { pub food: u32, pub wood: u32, pub stone: u32, #[serde(default)] pub tools: u32, } impl Goods { pub fn of(good: Good, quantity: u32) -> Self { let mut goods = Self::default(); *goods.get_mut(good) = quantity; goods } pub fn get(&self, good: Good) -> u32 { match good { Good::Food => self.food, Good::Wood => self.wood, Good::Stone => self.stone, Good::Tools => self.tools, Good::Carts => 0, } } /// The counter of `good`; carts are actors, not goods, so they have a scratch counter. pub fn get_mut(&mut self, good: Good) -> &mut u32 { match good { Good::Food => &mut self.food, Good::Wood => &mut self.wood, Good::Stone => &mut self.stone, Good::Tools | Good::Carts => &mut self.tools, } } pub fn total(&self) -> u32 { self.food + self.wood + self.stone + self.tools } pub fn add(&mut self, other: Goods) { for good in Good::GOODS { *self.get_mut(good) += other.get(good); } } } /// Something traded on a market. Carts are actors and are traded one at a time. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub(super) enum Good { Food, Wood, Stone, Tools, Carts, } impl Good { /// The goods that are carried as [`Goods`]. pub const GOODS: [Good; 4] = [Good::Food, Good::Wood, Good::Stone, Good::Tools]; pub const ALL: [Good; 5] = [ Good::Food, Good::Wood, Good::Stone, Good::Tools, Good::Carts, ]; pub fn index(self) -> usize { self as usize } } /// What a workplace does; merchants work for markets. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub(super) enum Trade { Farm, Woodcutter, Quarry, Hunter, Toolmaker, Cartwright, Merchant, } impl Trade { pub const WORKSHOPS: [Trade; 6] = [ Trade::Farm, Trade::Woodcutter, Trade::Quarry, Trade::Hunter, Trade::Toolmaker, Trade::Cartwright, ]; pub fn key(self) -> &'static str { match self { Trade::Farm => "farm", Trade::Woodcutter => "woodcutter", Trade::Quarry => "quarry", Trade::Hunter => "hunter", Trade::Toolmaker => "toolmaker", Trade::Cartwright => "cartwright", Trade::Merchant => "merchant", } } /// Whether workers buy inputs at the market and craft them at the workplace. pub fn crafts(self) -> bool { matches!(self, Trade::Toolmaker | Trade::Cartwright) } } /// Where a worker is in their shift. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub(super) enum Phase { /// On the way to the workplace to start the day's shift. Commute, /// Gathering at the workplace. Work, /// A crafter buying inputs at the market. Fetch, /// A crafter turning inputs into goods at the workplace. Craft, /// Taking the day's goods to the market and settling with the employer. Sell, /// A merchant taking goods to a market elsewhere. Travel, /// A merchant bringing back what they bought. Return, Done, } /// A person's employment, including the employer's goods and coins the worker carries. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub(super) struct Job { pub employer: EntityKey, pub trade: Trade, pub site: (usize, usize), pub market: EntityKey, pub market_site: (usize, usize), pub phase: Phase, /// The day the last shift ended. pub day: Option, pub goods: Goods, pub coins: u32, /// Carts the worker built and still has to sell. pub wares: Vec, /// The market a merchant is travelling to. pub destination: Option<(EntityKey, (usize, usize))>, } impl Job { pub fn new(employer: EntityKey, trade: Trade, site: (usize, usize), market: &Market) -> Self { Self { employer, trade, site, market: market.key(), market_site: market.site(), phase: Phase::Done, day: None, goods: Goods::default(), coins: 0, wares: Vec::new(), destination: None, } } pub fn shift_done(&self, today: u64) -> bool { self.phase == Phase::Done && self.day == Some(today) } pub fn on_shift(&self) -> bool { !matches!(self.phase, Phase::Done | Phase::Commute) } pub fn on_trip(&self) -> bool { matches!(self.phase, Phase::Travel | Phase::Return) } } /// Base prices and the stock each market aims to hold, per [`Good`]. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] pub(super) struct Pricing { pub base: [u32; 5], pub target: [u32; 5], pub rounds: u32, } impl Pricing { /// The market price: twice the base price when out of stock, the base price at the target /// stock, and falling towards zero as the stock grows. pub fn price(&self, good: Good, stock: u32) -> u32 { let base = u64::from(self.base[good.index()]); let target = u64::from(self.target[good.index()].max(1)); let price = base * 2 * target / (u64::from(stock) + target); u32::try_from(price).unwrap_or(u32::MAX).max(1) } pub fn base(&self, good: Good) -> u32 { self.base[good.index()].max(1) } } /// One side's opening offer and the worst unit price it accepts. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] pub(super) struct Stance { pub open: u32, pub limit: u32, } impl Stance { /// A seller asking half as much again as `price` and accepting no less than `limit`. pub fn seller(price: u32, limit: u32) -> Self { Self { open: (price + price / 2).max(limit), limit, } } /// A buyer opening at half of `price` and paying at most `limit`. pub fn buyer(price: u32, limit: u32) -> Self { Self { open: (price / 2).min(limit), limit, } } } /// Alternating concessions: every round, seller and buyer each give up a share of the gap /// between their opening offer and their limit, until the bid meets the ask. They settle in /// the middle; there is no deal when the seller's limit is above the buyer's. pub(super) fn haggle(seller: Stance, buyer: Stance, rounds: u32) -> Option { if seller.limit > buyer.limit { return None; } let seller_open = u64::from(seller.open.max(seller.limit)); let buyer_open = u64::from(buyer.open.min(buyer.limit)); let (seller_limit, buyer_limit) = (u64::from(seller.limit), u64::from(buyer.limit)); let rounds = u64::from(rounds.max(1)); for round in 0..=rounds { let ask = seller_open - (seller_open - seller_limit) * round / rounds; let bid = buyer_open + (buyer_limit - buyer_open) * round / rounds; if bid >= ask { return u32::try_from(ask.midpoint(bid)).ok(); } } Some(seller.limit) } /// The people working somewhere and what they are paid per shift. #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub(super) struct Staff { pub employees: Vec, pub positions: usize, pub wage: u32, } impl Staff { fn hire(&mut self, person: u32) -> bool { if self.employees.contains(&person) { return true; } if self.employees.len() >= self.positions { return false; } self.employees.push(person); true } /// Pays `person` their wage from `coins`, as far as the coins go. fn pay(&self, router: &Router, coins: &mut u32, person: u32) { if !self.employees.contains(&person) { return; } let amount = self.wage.min(*coins); *coins -= amount; router.tell( EntityKey::person(person), PersonMsg::Paid { amount, owed: self.wage, }, ); } } #[derive(Clone, Debug, Deserialize, Serialize)] pub(super) struct Workplace { pub id: u64, pub trade: Trade, pub x: usize, pub y: usize, pub market: EntityKey, pub market_site: (usize, usize), pub coins: u32, pub staff: Staff, /// Farm fields, where crops are sown and harvested. #[serde(default)] pub fields: Vec<(usize, usize)>, } impl Workplace { pub fn key(&self) -> EntityKey { EntityKey::new(Kind::Workplace, self.id) } pub fn site(&self) -> (usize, usize) { (self.x, self.y) } } #[derive(Clone, Debug, Deserialize, Serialize)] pub(super) enum WorkplaceMsg { Observe(RequestId), Hire(RequestId, u32), /// A worker leaves, handing back the coins and goods they held for the workplace. Resign { person: u32, coins: u32, }, /// A crafter starts their shift and takes up to `budget` coins to buy inputs. ClockIn { request: RequestId, budget: u32, }, /// A worker hands over the day's takings and gets paid. Settle { person: u32, coins: u32, }, } pub(super) struct WorkplaceActor { workplace: Workplace, router: Arc, } impl ActorFactoryArgs<(Workplace, Arc)> for WorkplaceActor { fn create_args((workplace, router): (Workplace, Arc)) -> Self { Self { workplace, router } } } impl Actor for WorkplaceActor { type Msg = WorkplaceMsg; fn recv(&mut self, _ctx: &ActorContext, msg: Self::Msg, sender: Sender) { let workplace = &mut self.workplace; match msg { WorkplaceMsg::Observe(request) => { reply(sender, request, ReplyBody::Workplace(workplace.clone())); } WorkplaceMsg::Hire(request, person) => { let hired = workplace.staff.hire(person); reply(sender, request, ReplyBody::Hired(hired)); } WorkplaceMsg::Resign { person, coins } => { workplace.coins += coins; workplace.staff.employees.retain(|&id| id != person); } WorkplaceMsg::ClockIn { request, budget } => { let advance = budget.min(workplace.coins); workplace.coins -= advance; reply( sender, request, ReplyBody::Consigned(Goods::default(), advance), ); } WorkplaceMsg::Settle { person, coins } => { workplace.coins += coins; workplace .staff .pay(&self.router, &mut workplace.coins, person); } } } } #[derive(Clone, Debug, Deserialize, Serialize)] pub(super) struct Market { pub id: u64, pub x: usize, pub y: usize, pub coins: u32, pub stock: Goods, /// Carts for sale, parked at the market. pub carts: Vec, /// The market's merchants. pub staff: Staff, } impl Market { pub fn key(&self) -> EntityKey { EntityKey::new(Kind::Market, self.id) } pub fn site(&self) -> (usize, usize) { (self.x, self.y) } pub fn stock_of(&self, good: Good) -> u32 { match good { Good::Carts => u32::try_from(self.carts.len()).unwrap_or(u32::MAX), good => self.stock.get(good), } } pub fn price(&self, pricing: &Pricing, good: Good) -> u32 { pricing.price(good, self.stock_of(good)) } /// The market's stance when it buys: it opens low and pays up to 90% of its price. fn bid(&self, pricing: &Pricing, good: Good) -> Stance { let price = self.price(pricing, good); Stance::buyer(price, (price * 9 / 10).max(1)) } /// The market's stance when it sells: it opens high and asks at least 110% of its price. fn ask(&self, pricing: &Pricing, good: Good) -> Stance { let price = self.price(pricing, good); Stance::seller(price, (price * 11 / 10).max(1)) } pub fn quote(&self, pricing: &Pricing) -> Quote { Quote { market: self.key(), site: self.site(), prices: Good::ALL.map(|good| self.price(pricing, good)), } } /// Buys up to `quantity` of `good` from a seller with `stance`. fn buy(&mut self, pricing: &Pricing, good: Good, quantity: u32, stance: Stance) -> Traded { let Some(price) = haggle(stance, self.bid(pricing, good), pricing.rounds) else { return Traded::NONE; }; let quantity = quantity.min(self.coins / price.max(1)); self.coins -= quantity * price; *self.stock.get_mut(good) += quantity; Traded { quantity, price } } /// Sells up to `quantity` of `good` to a buyer with `stance` who has `budget` coins. fn sell( &mut self, pricing: &Pricing, good: Good, quantity: u32, stance: Stance, budget: u32, ) -> Traded { let Some(price) = haggle(self.ask(pricing, good), stance, pricing.rounds) else { return Traded::NONE; }; let quantity = quantity .min(self.stock.get(good)) .min(budget / price.max(1)); self.coins += quantity * price; *self.stock.get_mut(good) -= quantity; Traded { quantity, price } } /// Hands a merchant the good the market holds the most surplus of, and coins to trade with. fn consign(&mut self, pricing: &Pricing, capacity: u32, escrow: u32) -> (Goods, u32) { let stock = self.stock; let surplus = |good: Good| stock.get(good).saturating_sub(pricing.target[good.index()]); let goods = Good::GOODS .into_iter() .max_by_key(|&good| surplus(good)) .filter(|&good| surplus(good) > 0) .map(|good| { let quantity = surplus(good).min(capacity); *self.stock.get_mut(good) -= quantity; Goods::of(good, quantity) }) .unwrap_or_default(); let coins = escrow.min(self.coins); self.coins -= coins; (goods, coins) } } /// The outcome of a trade: how much changed hands at which unit price. #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub(super) struct Traded { pub quantity: u32, pub price: u32, } impl Traded { const NONE: Traded = Traded { quantity: 0, price: 0, }; pub fn total(self) -> u32 { self.quantity * self.price } } /// Market prices as published to every shard, so that merchants can pick where to go. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct Quote { pub market: EntityKey, pub site: (usize, usize), pub prices: [u32; 5], } #[derive(Clone, Debug, Deserialize, Serialize)] pub(super) enum MarketMsg { Observe(RequestId), /// Someone with `stance` sells the market up to `quantity` of `good`. Sell { request: RequestId, good: Good, quantity: u32, stance: Stance, pricing: Pricing, }, /// Someone with `stance` and `budget` coins buys up to `quantity` of `good`. Buy { request: RequestId, good: Good, quantity: u32, stance: Stance, budget: u32, pricing: Pricing, }, /// A cartwright sells the market a cart. SellCart { request: RequestId, cart: u64, stance: Stance, pricing: Pricing, }, /// Someone buys one of the carts for sale. BuyCart { request: RequestId, stance: Stance, budget: u32, pricing: Pricing, }, Hire(RequestId, u32), Resign { person: u32, goods: Goods, coins: u32, }, /// A merchant starts a trip and takes goods and up to `escrow` coins. ClockIn { request: RequestId, capacity: u32, escrow: u32, pricing: Pricing, }, /// Goods handed over without payment, such as a leaving worker's unsold takings. Stock(Goods), /// A merchant hands over what they brought back and gets paid. Settle { person: u32, goods: Goods, coins: u32, }, } pub(super) struct MarketActor { market: Market, router: Arc, } impl ActorFactoryArgs<(Market, Arc)> for MarketActor { fn create_args((market, router): (Market, Arc)) -> Self { Self { market, router } } } impl Actor for MarketActor { type Msg = MarketMsg; fn recv(&mut self, _ctx: &ActorContext, msg: Self::Msg, sender: Sender) { let market = &mut self.market; match msg { MarketMsg::Observe(request) => { reply(sender, request, ReplyBody::Market(market.clone())); } MarketMsg::Sell { request, good, quantity, stance, pricing, } => { let traded = market.buy(&pricing, good, quantity, stance); reply(sender, request, ReplyBody::Traded(traded)); } MarketMsg::Buy { request, good, quantity, stance, budget, pricing, } => { let traded = market.sell(&pricing, good, quantity, stance, budget); reply(sender, request, ReplyBody::Traded(traded)); } MarketMsg::SellCart { request, cart, stance, pricing, } => { let bid = market.bid(&pricing, Good::Carts); let traded = haggle(stance, bid, pricing.rounds) .filter(|&price| price <= market.coins) .map_or(Traded::NONE, |price| { market.coins -= price; market.carts.push(cart); Traded { quantity: 1, price } }); reply(sender, request, ReplyBody::Traded(traded)); } MarketMsg::BuyCart { request, stance, budget, pricing, } => { let ask = market.ask(&pricing, Good::Carts); let bought = haggle(ask, stance, pricing.rounds) .filter(|&price| price <= budget && !market.carts.is_empty()) .map(|price| { market.coins += price; (market.carts.remove(0), price) }); reply(sender, request, ReplyBody::CartBought(bought)); } MarketMsg::Hire(request, person) => { let hired = market.staff.hire(person); reply(sender, request, ReplyBody::Hired(hired)); } MarketMsg::Resign { person, goods, coins, } => { market.stock.add(goods); market.coins += coins; market.staff.employees.retain(|&id| id != person); } MarketMsg::ClockIn { request, capacity, escrow, pricing, } => { let (goods, coins) = market.consign(&pricing, capacity, escrow); reply(sender, request, ReplyBody::Consigned(goods, coins)); } MarketMsg::Stock(goods) => market.stock.add(goods), MarketMsg::Settle { person, goods, coins, } => { market.stock.add(goods); market.coins += coins; market.staff.pay(&self.router, &mut market.coins, person); } } } } #[derive(Clone, Debug, Deserialize, Serialize)] pub(super) struct Cart { pub id: u64, pub x: usize, pub y: usize, pub owner: Option, /// Tiles travelled so far; the cart breaks once it has travelled its durability. pub wear: u32, pub broken: bool, } impl Cart { pub fn key(&self) -> EntityKey { EntityKey::new(Kind::Cart, self.id) } pub fn position(&self) -> (usize, usize) { (self.x, self.y) } } #[derive(Clone, Debug, Deserialize, Serialize)] pub(super) enum CartMsg { Observe(RequestId), /// Follows the owner to `to`, wearing out on the way. Follow { request: RequestId, to: (usize, usize), durability: u32, }, SetOwner(Option), MoveTo((usize, usize)), } pub(super) struct CartActor { cart: Cart, } impl ActorFactoryArgs for CartActor { fn create_args(cart: Cart) -> Self { Self { cart } } } impl Actor for CartActor { type Msg = CartMsg; fn recv(&mut self, _ctx: &ActorContext, msg: Self::Msg, sender: Sender) { let cart = &mut self.cart; match msg { CartMsg::Observe(request) => reply(sender, request, ReplyBody::Cart(cart.clone())), CartMsg::Follow { request, to, durability, } => { let distance = cart.x.abs_diff(to.0).max(cart.y.abs_diff(to.1)); cart.wear = cart .wear .saturating_add(u32::try_from(distance).unwrap_or(u32::MAX)); (cart.x, cart.y) = to; cart.broken |= cart.wear >= durability; reply(sender, request, ReplyBody::Cart(cart.clone())); } CartMsg::SetOwner(owner) => cart.owner = owner, CartMsg::MoveTo(position) => (cart.x, cart.y) = position, } } } #[cfg(test)] mod tests { use super::{Good, Goods, Market, Pricing, Staff, Stance, haggle}; fn pricing() -> Pricing { Pricing { base: [4, 2, 3, 14, 40], target: [60, 40, 30, 8, 2], rounds: 4, } } #[test] fn haggling_meets_between_the_limits_or_fails() { let seller = Stance { open: 12, limit: 4 }; let buyer = Stance { open: 2, limit: 8 }; let price = haggle(seller, buyer, 4).unwrap(); assert!((4..=8).contains(&price)); assert_eq!(haggle(Stance { open: 12, limit: 9 }, buyer, 4), None); // A stubborn seller pulls the price up. let stubborn = haggle(Stance { open: 30, limit: 4 }, buyer, 4).unwrap(); assert!(stubborn >= price); } #[test] fn prices_follow_supply() { let pricing = pricing(); assert_eq!(pricing.price(Good::Food, 0), 8); assert_eq!(pricing.price(Good::Food, 60), 4); assert!(pricing.price(Good::Food, 600) < 4); assert!(pricing.price(Good::Food, 1_000_000) >= 1); } #[test] fn markets_trade_within_their_coins_and_stock() { let pricing = pricing(); let mut market = Market { id: 1, x: 0, y: 0, coins: 10, stock: Goods::default(), carts: Vec::new(), staff: Staff::default(), }; let sold = market.buy(&pricing, Good::Food, 100, Stance::seller(8, 1)); assert!(sold.quantity > 0 && sold.total() <= 10); assert_eq!(market.coins + sold.total(), 10); assert_eq!(market.stock.food, sold.quantity); let bought = market.sell(&pricing, Good::Food, 100, Stance::buyer(4, 100), 1000); assert_eq!(bought.quantity, sold.quantity); assert_eq!(market.stock.food, 0); assert!( bought.price >= sold.price, "the market buys low and sells high" ); } }