sim/src/world/economy.rs

844 lines
25 KiB
Rust
Raw Normal View History

//! 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::{Admission, 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<u64>,
pub goods: Goods,
pub coins: u32,
/// Carts the worker built and still has to sell.
pub wares: Vec<u64>,
/// 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<u32> {
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<u32>,
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<Router>,
}
impl ActorFactoryArgs<(Workplace, Arc<Router>)> for WorkplaceActor {
fn create_args((workplace, router): (Workplace, Arc<Router>)) -> Self {
Self { workplace, router }
}
}
impl Actor for WorkplaceActor {
type Msg = WorkplaceMsg;
fn recv(&mut self, _ctx: &ActorContext<Self::Msg>, 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<u64>,
/// 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<Router>,
}
impl ActorFactoryArgs<(Market, Arc<Router>)> for MarketActor {
fn create_args((market, router): (Market, Arc<Router>)) -> Self {
Self { market, router }
}
}
impl Actor for MarketActor {
type Msg = MarketMsg;
fn recv(&mut self, _ctx: &ActorContext<Self::Msg>, 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<u32>,
/// 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<u32>),
MoveTo((usize, usize)),
Depart(RequestId, usize),
Readmit(Cart),
}
pub(super) struct CartActor {
cart: Cart,
router: Arc<Router>,
forward_to: Option<usize>,
}
impl ActorFactoryArgs<(Cart, Arc<Router>)> for CartActor {
fn create_args((cart, router): (Cart, Arc<Router>)) -> Self {
Self {
cart,
router,
forward_to: None,
}
}
}
impl Actor for CartActor {
type Msg = CartMsg;
fn recv(&mut self, _ctx: &ActorContext<Self::Msg>, msg: Self::Msg, sender: Sender) {
if let Some(shard) = self.forward_to {
if let CartMsg::Readmit(cart) = msg {
self.cart = cart;
self.forward_to = None;
} else {
self.router.forward(shard, self.cart.key(), &msg);
}
return;
}
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,
CartMsg::Depart(request, shard) => {
self.router.admit_at(shard, Admission::Cart(cart.clone()));
self.forward_to = Some(shard);
reply(sender, request, ReplyBody::Departed);
}
CartMsg::Readmit(readmitted) => *cart = readmitted,
}
}
}
#[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"
);
}
}