821 lines
24 KiB
Rust
821 lines
24 KiB
Rust
//! 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<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)),
|
|
}
|
|
|
|
pub(super) struct CartActor {
|
|
cart: Cart,
|
|
}
|
|
|
|
impl ActorFactoryArgs<Cart> for CartActor {
|
|
fn create_args(cart: Cart) -> Self {
|
|
Self { cart }
|
|
}
|
|
}
|
|
|
|
impl Actor for CartActor {
|
|
type Msg = CartMsg;
|
|
|
|
fn recv(&mut self, _ctx: &ActorContext<Self::Msg>, 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"
|
|
);
|
|
}
|
|
}
|