use crate::wallet::{
credit_cx, debit_cx, ensure_economy_boot, load_balance, load_orders, load_profile,
next_order_id, push_intent, save_orders, stock_add, stock_consume, stock_has, stock_qty,
MarketOrder,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GoodKind {
Element,
Product,
}
#[derive(Clone, Copy, Debug)]
pub struct GoodDef {
pub id: &'static str,
pub name: &'static str,
pub unit: &'static str,
pub kind: GoodKind,
pub class: &'static str, pub blurb: &'static str,
}
pub const GOODS: &[GoodDef] = &[
GoodDef {
id: "energy",
name: "ENERGY",
unit: "kWh",
kind: GoodKind::Element,
class: "energy",
blurb: "Electric work โ solar, genset, grid soft.",
},
GoodDef {
id: "water",
name: "WATER",
unit: "mยณ",
kind: GoodKind::Element,
class: "matter",
blurb: "Stored / pumped water for grow and camp.",
},
GoodDef {
id: "labor",
name: "LABOR",
unit: "h",
kind: GoodKind::Element,
class: "labor",
blurb: "Person-hours โ you or robots as soft stock.",
},
GoodDef {
id: "wood",
name: "WOOD",
unit: "mยณ",
kind: GoodKind::Element,
class: "matter",
blurb: "Timber / biomass feedstock.",
},
GoodDef {
id: "food_raw",
name: "FOOD RAW",
unit: "kg",
kind: GoodKind::Element,
class: "food",
blurb: "Unprocessed garden / market produce.",
},
GoodDef {
id: "fill",
name: "FILL",
unit: "mยณ",
kind: GoodKind::Element,
class: "matter",
blurb: "Earth fill for terraces and pads.",
},
GoodDef {
id: "gravel",
name: "GRAVEL",
unit: "t",
kind: GoodKind::Element,
class: "matter",
blurb: "Aggregate for trails and bases.",
},
GoodDef {
id: "biochar",
name: "BIOCHAR",
unit: "kg",
kind: GoodKind::Element,
class: "matter",
blurb: "Carbon fixed in soil โ burn.city metabolism.",
},
GoodDef {
id: "bandwidth",
name: "BANDWIDTH",
unit: "GB",
kind: GoodKind::Element,
class: "digital",
blurb: "Uplink / mesh transfer budget.",
},
GoodDef {
id: "compute",
name: "COMPUTE",
unit: "GPUยทh",
kind: GoodKind::Element,
class: "digital",
blurb: "Local or rented inference / train hours.",
},
GoodDef {
id: "meal",
name: "MEAL",
unit: "ea",
kind: GoodKind::Product,
class: "food",
blurb: "Cooked meal โ kitchen product.",
},
GoodDef {
id: "camp_kit",
name: "CAMP KIT",
unit: "kit",
kind: GoodKind::Product,
class: "kit",
blurb: "Overnight camp build kit.",
},
GoodDef {
id: "cube_kit",
name: "CUBE KIT",
unit: "kit",
kind: GoodKind::Product,
class: "kit",
blurb: "Cube / hard-shell build stack.",
},
GoodDef {
id: "trail_kit",
name: "TRAIL KIT",
unit: "kit",
kind: GoodKind::Product,
class: "kit",
blurb: "Trail repair package.",
},
GoodDef {
id: "biochar_bag",
name: "BIOCHAR BAG",
unit: "bag",
kind: GoodKind::Product,
class: "kit",
blurb: "Bagged biochar for soil.",
},
GoodDef {
id: "soft_night",
name: "SOFT NIGHT",
unit: "night",
kind: GoodKind::Product,
class: "stay",
blurb: "Hospitality night โ meal + energy + care.",
},
];
pub fn good(id: &str) -> Option<&'static GoodDef> {
GOODS.iter().find(|g| g.id == id)
}
pub fn elements() -> impl Iterator<Item = &'static GoodDef> {
GOODS.iter().filter(|g| g.kind == GoodKind::Element)
}
pub fn products() -> impl Iterator<Item = &'static GoodDef> {
GOODS.iter().filter(|g| g.kind == GoodKind::Product)
}
#[derive(Clone, Copy, Debug)]
pub struct BomIo {
pub id: &'static str,
pub qty: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct BomRecipe {
pub id: &'static str,
pub name: &'static str,
pub blurb: &'static str,
pub inputs: &'static [BomIo],
pub outputs: &'static [BomIo],
pub labor_note: &'static str,
}
pub const BOMS: &[BomRecipe] = &[
BomRecipe {
id: "cut_wood",
name: "CUT WOOD",
blurb: "Labor + energy โ timber.",
inputs: &[
BomIo {
id: "labor",
qty: 2.0,
},
BomIo {
id: "energy",
qty: 1.0,
},
],
outputs: &[BomIo {
id: "wood",
qty: 1.0,
}],
labor_note: "field cut",
},
BomRecipe {
id: "pump_water",
name: "PUMP WATER",
blurb: "Energy + labor โ stored water.",
inputs: &[
BomIo {
id: "energy",
qty: 2.0,
},
BomIo {
id: "labor",
qty: 0.5,
},
],
outputs: &[BomIo {
id: "water",
qty: 1.0,
}],
labor_note: "pump run",
},
BomRecipe {
id: "grow_food",
name: "GROW FOOD",
blurb: "Water + labor + energy โ raw food.",
inputs: &[
BomIo {
id: "water",
qty: 1.0,
},
BomIo {
id: "labor",
qty: 3.0,
},
BomIo {
id: "energy",
qty: 1.0,
},
],
outputs: &[BomIo {
id: "food_raw",
qty: 2.0,
}],
labor_note: "garden",
},
BomRecipe {
id: "cook_meal",
name: "COOK MEAL",
blurb: "Raw food + energy + labor โ meal.",
inputs: &[
BomIo {
id: "food_raw",
qty: 1.0,
},
BomIo {
id: "energy",
qty: 1.5,
},
BomIo {
id: "labor",
qty: 0.5,
},
],
outputs: &[BomIo {
id: "meal",
qty: 1.0,
}],
labor_note: "kitchen",
},
BomRecipe {
id: "make_biochar",
name: "MAKE BIOCHAR",
blurb: "Wood + labor โ biochar (burn to soil).",
inputs: &[
BomIo {
id: "wood",
qty: 1.0,
},
BomIo {
id: "labor",
qty: 2.0,
},
BomIo {
id: "energy",
qty: 0.5,
},
],
outputs: &[BomIo {
id: "biochar",
qty: 4.0,
}],
labor_note: "kiln",
},
BomRecipe {
id: "bag_biochar",
name: "BAG BIOCHAR",
blurb: "Biochar โ bagged product.",
inputs: &[
BomIo {
id: "biochar",
qty: 5.0,
},
BomIo {
id: "labor",
qty: 0.5,
},
],
outputs: &[BomIo {
id: "biochar_bag",
qty: 1.0,
}],
labor_note: "pack",
},
BomRecipe {
id: "camp_kit",
name: "ASSEMBLE CAMP KIT",
blurb: "Wood + labor โ camp kit.",
inputs: &[
BomIo {
id: "wood",
qty: 2.0,
},
BomIo {
id: "labor",
qty: 4.0,
},
BomIo {
id: "energy",
qty: 1.0,
},
],
outputs: &[BomIo {
id: "camp_kit",
qty: 1.0,
}],
labor_note: "yard",
},
BomRecipe {
id: "cube_kit",
name: "ASSEMBLE CUBE KIT",
blurb: "Wood + fill + gravel + labor + energy โ cube kit.",
inputs: &[
BomIo {
id: "wood",
qty: 3.0,
},
BomIo {
id: "fill",
qty: 2.0,
},
BomIo {
id: "gravel",
qty: 1.0,
},
BomIo {
id: "labor",
qty: 8.0,
},
BomIo {
id: "energy",
qty: 4.0,
},
],
outputs: &[BomIo {
id: "cube_kit",
qty: 1.0,
}],
labor_note: "build stack",
},
BomRecipe {
id: "trail_kit",
name: "ASSEMBLE TRAIL KIT",
blurb: "Gravel + labor + energy โ trail kit.",
inputs: &[
BomIo {
id: "gravel",
qty: 1.0,
},
BomIo {
id: "labor",
qty: 3.0,
},
BomIo {
id: "energy",
qty: 1.0,
},
],
outputs: &[BomIo {
id: "trail_kit",
qty: 1.0,
}],
labor_note: "base crew",
},
BomRecipe {
id: "soft_night",
name: "HOST SOFT NIGHT",
blurb: "Meal + energy + labor โ hospitality night.",
inputs: &[
BomIo {
id: "meal",
qty: 1.0,
},
BomIo {
id: "energy",
qty: 3.0,
},
BomIo {
id: "labor",
qty: 2.0,
},
BomIo {
id: "bandwidth",
qty: 1.0,
},
],
outputs: &[BomIo {
id: "soft_night",
qty: 1.0,
}],
labor_note: "host",
},
BomRecipe {
id: "solar_charge",
name: "SOLAR CHARGE",
blurb: "Labor (panel attend) โ energy.",
inputs: &[BomIo {
id: "labor",
qty: 1.0,
}],
outputs: &[BomIo {
id: "energy",
qty: 5.0,
}],
labor_note: "array",
},
BomRecipe {
id: "dig_fill",
name: "DIG FILL",
blurb: "Labor + energy โ fill earth.",
inputs: &[
BomIo {
id: "labor",
qty: 3.0,
},
BomIo {
id: "energy",
qty: 2.0,
},
],
outputs: &[BomIo {
id: "fill",
qty: 2.0,
}],
labor_note: "terrace",
},
];
pub fn bom(id: &str) -> Option<&'static BomRecipe> {
BOMS.iter().find(|b| b.id == id)
}
fn needs_of(recipe: &BomRecipe) -> Vec<(String, f64)> {
recipe
.inputs
.iter()
.map(|i| (i.id.to_string(), i.qty))
.collect()
}
pub fn run_bom(recipe_id: &str) -> Result<String, String> {
ensure_economy_boot();
let recipe = bom(recipe_id).ok_or_else(|| "unknown recipe".to_string())?;
let needs = needs_of(recipe);
if !stock_has(&needs) {
let missing: Vec<String> = needs
.iter()
.filter(|(id, q)| stock_qty(id) + 1e-9 < *q)
.map(|(id, q)| format!("{id} need {q} have {:.1}", stock_qty(id)))
.collect();
return Err(format!("missing: {}", missing.join(", ")));
}
if !stock_consume(&needs) {
return Err("consume failed".into());
}
for o in recipe.outputs {
stock_add(o.id, o.qty);
}
let out_s: Vec<String> = recipe
.outputs
.iter()
.map(|o| format!("+{} {}", o.qty, o.id))
.collect();
push_intent(
"YOU",
"bom",
&format!("{} โ {}", recipe.id, out_s.join(" ")),
);
let mut b = load_balance();
b.depth = (b.depth + 1.5).min(99.0);
crate::wallet::save_balance(&b);
Ok(format!("{} ยท {}", recipe.name, out_s.join(", ")))
}
pub fn market_buy(order_id: u64, qty: f64) -> Result<String, String> {
ensure_economy_boot();
if qty <= 0.0 {
return Err("qty must be > 0".into());
}
let mut orders = load_orders();
let idx = orders
.iter()
.position(|o| o.id == order_id && o.side == "sell")
.ok_or_else(|| "order not found".to_string())?;
let order = orders[idx].clone();
let take = qty.min(order.qty);
let cost = take * order.price_cx;
let bal = load_balance();
if bal.cx + 1e-9 < cost {
return Err(format!("need {cost:.1} CX, have {:.1}", bal.cx));
}
debit_cx(cost, "products", &format!("buy {take} {}", order.good_id));
stock_add(&order.good_id, take);
if take + 1e-9 >= order.qty {
orders.remove(idx);
} else {
orders[idx].qty -= take;
}
save_orders(&orders);
push_intent(
"YOU",
"buy",
&format!("{} x{take} @ {:.2} CX", order.good_id, order.price_cx),
);
Ok(format!("bought {take} {} for {cost:.1} CX", order.good_id))
}
pub fn market_list_sell(good_id: &str, qty: f64, price_cx: f64) -> Result<String, String> {
ensure_economy_boot();
if qty <= 0.0 || price_cx <= 0.0 {
return Err("qty and price must be > 0".into());
}
if good(good_id).is_none() {
return Err("unknown good".into());
}
if stock_qty(good_id) + 1e-9 < qty {
return Err(format!(
"have {:.1} {}, need {qty}",
stock_qty(good_id),
good_id
));
}
stock_add(good_id, -qty);
let handle = load_profile().handle;
let mut orders = load_orders();
let id = next_order_id();
orders.insert(
0,
MarketOrder {
id,
good_id: good_id.into(),
qty,
price_cx,
side: "sell".into(),
owner: handle.clone(),
},
);
save_orders(&orders);
push_intent(
&handle,
"list",
&format!("sell {qty} {good_id} @ {price_cx} CX"),
);
Ok(format!("listed {qty} {good_id} @ {price_cx} CX"))
}
pub fn market_cancel(order_id: u64) -> Result<String, String> {
let handle = load_profile().handle;
let mut orders = load_orders();
let idx = orders
.iter()
.position(|o| o.id == order_id && o.owner == handle)
.ok_or_else(|| "not your order".to_string())?;
let o = orders.remove(idx);
if o.side == "sell" {
stock_add(&o.good_id, o.qty);
}
save_orders(&orders);
push_intent(&handle, "cancel", &format!("order #{}", o.id));
Ok(format!("cancelled #{}", o.id))
}
pub fn market_sell_to_city(good_id: &str, qty: f64) -> Result<String, String> {
ensure_economy_boot();
if qty <= 0.0 {
return Err("qty must be > 0".into());
}
if stock_qty(good_id) + 1e-9 < qty {
return Err("insufficient stock".into());
}
let asks: Vec<f64> = load_orders()
.into_iter()
.filter(|o| o.good_id == good_id && o.side == "sell" && o.owner == "cyber-valley")
.map(|o| o.price_cx)
.collect();
let px = asks
.into_iter()
.fold(None, |a: Option<f64>, b| {
Some(match a {
Some(x) => x.min(b),
None => b,
})
})
.map(|p| p * 0.70)
.unwrap_or(1.0);
stock_add(good_id, -qty);
let rev = qty * px;
credit_cx(rev, "products", &format!("sell {qty} {good_id} โ city"));
push_intent(
"YOU",
"sell",
&format!("{good_id} x{qty} โ city @ {px:.2} CX"),
);
Ok(format!("sold {qty} {good_id} to city for {rev:.1} CX"))
}
pub fn fmt_qty(q: f64) -> String {
if (q - q.round()).abs() < 1e-6 {
format!("{:.0}", q)
} else if q >= 10.0 {
format!("{q:.1}")
} else {
format!("{q:.2}")
}
}
pub fn city_ask(good_id: &str) -> Option<f64> {
load_orders()
.into_iter()
.filter(|o| o.good_id == good_id && o.side == "sell" && o.owner == "cyber-valley")
.map(|o| o.price_cx)
.fold(None, |a, b| Some(a.map_or(b, |x: f64| x.min(b))))
}
pub fn product_buy(good_id: &str, qty: f64) -> Result<String, String> {
ensure_economy_boot();
if qty <= 0.0 {
return Err("qty must be > 0".into());
}
let mut orders = load_orders();
let idx = orders
.iter()
.enumerate()
.filter(|(_, o)| o.good_id == good_id && o.side == "sell" && o.owner == "cyber-valley")
.min_by(|a, b| a.1.price_cx.partial_cmp(&b.1.price_cx).unwrap())
.map(|(i, _)| i)
.ok_or_else(|| "city has no ask for this good".to_string())?;
let px = orders[idx].price_cx;
let take = qty.min(orders[idx].qty);
let cost = take * px;
if load_balance().cx + 1e-9 < cost {
return Err(format!("need {cost:.1} CX, have {:.1}", load_balance().cx));
}
debit_cx(cost, "products", &format!("buy {take} {good_id}"));
stock_add(good_id, take);
orders[idx].qty -= take;
if orders[idx].qty <= 1e-9 {
orders.remove(idx);
}
save_orders(&orders);
let handle = load_profile().handle;
push_intent(&handle, "buy", &format!("{good_id} x{take} @ {px:.2} CX"));
trade_signal(good_id, take, cost, false);
Ok(format!("bought {take} {good_id} ยท -{cost:.1} CX"))
}
pub fn product_sell(good_id: &str, qty: f64) -> Result<String, String> {
let msg = market_sell_to_city(good_id, qty)?;
let px = city_ask(good_id).map(|p| p * 0.70).unwrap_or(1.0);
trade_signal(good_id, qty, qty * px, true);
Ok(msg)
}
fn trade_signal(good_id: &str, qty: f64, total: f64, sell: bool) {
use crate::signal::{emit_signal, mint_word, neuron, Link};
let me = neuron().bech32;
let handle = load_profile().handle;
let you = mint_word("person", &handle, "YOU", &me, false);
let coin = mint_word("coin", good_id, "", &me, false);
let rel = if sell {
mint_word("relation", "sells", "market disposal", "", true)
} else {
mint_word("relation", "buys", "market acquisition", "", true)
};
let _ = emit_signal(
vec![Link {
from: you,
rel,
to: coin,
weight: qty,
note: format!("{}{total:.2} CX", if sell { "+" } else { "-" }),
}],
&format!("{} {qty} {good_id}", if sell { "sell" } else { "buy" }),
);
}