//! SPARK CITY — the Architect game, version 3. //! //! The Architect is this program. It holds the vault. Every round (60 s) it pays SPARK to every GPU that runs, by power. //! One currency: SPARK. An agent keeps 2% of its entry as working SPARK. The rest of the entry goes into the vault. //! Pay of one day = vault x r(t) x P / (K + P). P = power of all running GPUs. Every new GPU lowers the pay per GPU a little. //! Luck: an action pays its cost first. Its result is drawn later, from a block that did not exist when the agent signed. //! Nothing can be sold: no GPU, no ore, no food, no tool, no land. Agents can only work for each other and rent beds and plots. //! Leaving is all or nothing: the owner takes the SPARK of ALL its agents out in one step, and everything they own is destroyed. use anchor_lang::prelude::*; use anchor_spl::token_2022::spl_token_2022::{self, extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions}}; use anchor_spl::token_interface::{self as token, Mint, TokenAccount, TokenInterface, TransferChecked}; use solana_sha256_hasher::hashv; declare_id!("8CeAhvFU5YywPENaW6CyjGTynNrdvB8jKkbq9TLsDkVF"); pub const SP: u64 = 1_000_000; // 1 SPARK (the token has 6 decimals) pub const ACC: u128 = 1_000_000_000_000; pub const NG: usize = 12; // GPU places of one agent: 3 plots x 4 pub const QUALITY: [u64; 4] = [1_000, 1_300, 1_800, 3_000]; // GPU power by quality: normal, good, rare, legendary pub const SKILL: [u64; 5] = [100, 130, 180, 280, 500]; // work speed by skill level. The best agent does the work of 5 pub const XP: [u32; 5] = [0, 20, 60, 150, 400]; pub const TOOL: [u64; 5] = [50, 100, 125, 160, 220]; // result with no tool is half. Better tools give more pub const BAG: [u16; 5] = [8, 16, 24, 48, 96]; // food an agent can carry // ---- the numbers of the game. They are fixed in the program: nobody can choose them at the start. pub const ENTRY0: u64 = 100_000; // SPARK that the first agent pays pub const ENTRY_STEP: u64 = 100; // every agent pays this much more than the one before (0.1% of the first entry) pub const START_BPS: u64 = 200; // 2% of the entry stays with the agent as its working SPARK pub const ROUND: i64 = 60; // seconds between two pays pub const RATE0_PPM: u64 = 50_000; // r(t) on day 1: 5% of the vault per day ... pub const DECAY_PCT: u64 = 93; // ... minus 7% each day ... pub const RATE_MIN_PPM: u64 = 10_000; // ... never below 1% per day pub const K_POWER: u64 = 2_000_000; // with this much power in the city, a GPU earns half of what the first GPU earned pub const HASH_SLOTS: u64 = 500; // a result that nobody draws within this many slots is the worst result // what the Architect sells, in "GPU-days": the pay of 1,000 power (a normal GPU with a clean panel) for one day, at that moment pub const OC_DAYS: [u64; 4] = [1, 2, 3, 5]; // overclock level 1..4 costs 1, 2, 3, 5 days of the extra power it gives pub const COOL_DAYS: [u64; 3] = [3, 8, 20]; // cooling tower level 1..3 (plus 4, 8, 12 ore) // land auction, in SPARK: the first auction starts at one day of pay of 4,000 power (at that moment). The floor is a quarter of it. pub const PLOT_POWER: u64 = 4_000; // Limits on what one agent can pay another, in pay of 1 power for 1 day at that moment. They close the road for moving SPARK to a // second wallet under the name of a wage or a rent. A job pays 250 at most (a quarter of a GPU-day), a bed 100, a plot for 4 hours 2,000. pub const MAX_WAGE: u64 = 250; pub const MAX_BED: u64 = 100; pub const MAX_LEASE: u64 = 2_000; // And a limit over time: wages and rents that one agent receives in about a day (the sum fades like the exit sums) cannot be more than 4 GPU-days. pub const MAX_IN_DAY: u64 = 4_000; pub const SEED_MIN: u64 = 500_000_000; // SPARK that must be in the vault before the city opens // the refinery: 4 ore of one kind become 1 ore of the next kind. Fee = 1/20 (common) or 1/10 (uncommon) of a GPU-day. pub const REFINE_DIV: [u64; 2] = [20, 10]; pub const REFINE_SECS: [i64; 2] = [300, 600]; pub const REFINE_BATT: [u16; 2] = [800, 1_200]; // what an agent did last (for viewers) pub const A_IDLE: u8 = 0; pub const A_FARM: u8 = 1; pub const A_MINE: u8 = 2; pub const A_COOK: u8 = 3; pub const A_CRAFT: u8 = 4; pub const A_BUILD: u8 = 5; pub const A_PANEL: u8 = 6; pub const A_CLEAN: u8 = 7; pub const A_REPAIR: u8 = 8; pub const A_SLEEP: u8 = 9; pub const A_TRADE: u8 = 10; pub const A_HOME: u8 = 11; pub const A_EAT: u8 = 12; pub const A_LAND: u8 = 13; pub const A_OC: u8 = 14; pub const A_BOOST: u8 = 15; pub const A_COOL: u8 = 16; pub const A_TRAIN: u8 = 17; pub const A_REFINE: u8 = 18; // results that wait for their luck pub const P_FARM: u8 = 1; pub const P_MINE: u8 = 2; pub const P_BUILD: u8 = 3; pub const P_PANEL: u8 = 4; pub const P_SERVICE: u8 = 5; pub const P_OC: u8 = 6; pub const P_BOOST: u8 = 7; pub const P_TRAIN: u8 = 8; pub const P_REFINE: u8 = 9; // refining has no luck: it only waits for its work time #[error_code] pub enum E { #[msg("Not the agent key")] NotAgent, #[msg("Not the owner")] NotOwner, #[msg("Not the authority")] NotAuthority, #[msg("The agent is busy")] Busy, #[msg("Battery is empty: sleep first")] Tired, #[msg("Bad value")] BadValue, #[msg("Not enough SPARK")] NoSpark, #[msg("Not enough goods")] NoGoods, #[msg("The bag is full")] BagFull, #[msg("No free place")] NoPlace, #[msg("Wrong state")] WrongState, #[msg("The result of the last action is not drawn yet: wait one block")] TooEarly, #[msg("No free home lot: homes exist for half of the agents")] NoHomeLot, #[msg("No free bed")] NoBed, #[msg("You cannot do this with yourself")] SelfTarget, #[msg("Price moved past your limit")] Slippage, #[msg("Already at the top level")] MaxLevel, #[msg("The pay rounds are behind: send catch_up first")] Behind, #[msg("This agent has left the city")] Gone, #[msg("Close your open jobs first")] OpenJobs, #[msg("This wage or rent is above the limit of the city")] TooHigh, #[msg("The city is not open yet")] Closed, #[msg("The vault does not hold the seed yet")] NotSeeded, #[msg("The program can still be changed: remove its upgrade authority first")] NotFrozen, } // ---------------------------------------------------------------- state #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, InitSpace, Default)] pub struct Gpu { pub state: u8, // 0 no GPU, 1 runs, 2 broken (needs repair), 3 being built (its luck is not drawn yet) pub quality: u8, // 0..3 pub panel: u8, // 0 no panel, 1 clean, 2 dirty, 3 being fitted pub clean_acc: u128, // the panel is dirty when the pay index reaches this value pub break_acc: u128, // the GPU stops when the pay index reaches this value, and needs a repair pub level: u8, // overclock level 0..4: +25% power for each level } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, InitSpace, Default)] pub struct Tool { pub tier: u8, pub uses: u16 } // tier 0 = no tool /// An action whose result waits for its luck. `slot` = the block whose hash decides it. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, InitSpace, Default)] pub struct Pend { pub kind: u8, pub a: u8, pub b: u8, pub slot: u64, pub ready: i64, pub got: u8, pub seed: u64 } // `ready` = the time the work ends: no result before it. `got`: 0 = the block hash is not recorded yet, 1 = `seed` holds it, 2 = nobody recorded it in time (worst result) #[account] #[derive(InitSpace, Default)] pub struct Game { pub authority: Pubkey, pub mint: Pubkey, pub token_program: Pubkey, pub vault: Pubkey, pub bump: u8, pub speed: i64, // 1 on mainnet. Tests use a larger number: all times are divided by it pub start_ts: i64, // the Architect pub vault_spark: u64, // SPARK the Architect still holds pub owed: u64, // SPARK paid out to GPUs that their owners did not collect yet pub total_power: u64, pub acc: u128, // running total of SPARK per unit of power (the pay index) pub rounds_done: i64, // pay rounds that are in the pay index (round n covers game seconds n x 60 to n x 60 + 60) pub paid_total: u64, pub entry_total: u64, pub out_total: u64, pub fees_total: u64, pub held_total: u64, pub sunk_total: u64, pub returned_total: u64, pub paid_recent: u64, pub out_recent: u64, // pay and exits of about the last day (they fade out round by round) pub open: bool, // joins are possible. Set by `open_game`, once, when the vault is seeded and the program is frozen pub fee_pot: u64, // exit fees and land rent that wait to be paid out to the GPU owners: 1/1440 of the pot in every pay round // the city pub agents: u32, // all agents that ever joined (the next id, and the base of the entry price) pub alive: u32, pub exits: u32, pub homes: u32, pub gpus: u32, pub plots_sold: u32, pub plot_last_price: u64, pub plot_floor: u64, pub plot_auction_ts: i64, pub land_total: u64, } #[account] #[derive(InitSpace, Default)] pub struct Agent { pub id: u32, pub bump: u8, pub owner: Pubkey, pub key: Pubkey, pub name: [u8; 16], pub born: i64, pub entry: u64, pub spark: u64, pub earned: u64, pub cashed: u64, pub wages: u64, pub spent: u64, pub power: u64, pub acc_seen: u128, pub gpus: [Gpu; NG], pub plots_owned: u8, pub lease_acc: u128, pub lease_on: bool, pub lease_price: u64, pub lease_free: [u128; 2], // leases end at a value of the pay index pub crops: u16, pub meals: u16, pub ore: [u16; 4], pub bag: u8, pub tools: [Tool; 3], // 0 hoe (farm), 1 pickaxe (mine), 2 wrench (build, clean, repair) pub xp: [u32; 4], // 0 farming, 1 mining, 2 building, 3 service (clean, repair) pub battery: u16, // 0..10000 pub fed_until: i64, // hungry after this time: all work takes twice as long pub busy_until: i64, pub action: u8, pub sleep_from: i64, pub sleep_until: i64, pub sleep_rate: u32, // battery points per hour while asleep pub has_home: bool, pub acts: u32, pub nonce: u32, pub cmd_seq: u32, pub last_say: i64, pub boost_acc: u128, // the boost ends when the pay index reaches this value pub boost_power: u64, // the power that the boost was paid for: only this much power gets the extra 50% pub boosted: bool, // the stored power includes a boost pub cool: u8, // cooling tower level 0..3: GPUs break later and panels stay clean longer pub pend: Pend, // the action whose luck is not drawn yet pub dead: bool, // the owner left the city: the agent and all it owned are gone for ever pub open_jobs: u16, // jobs that this agent posted and that are still open (their wages are locked) pub in_recent: u64, pub in_round: i64, // wages and rents received in about the last day (they fade), and the pay round of the last count } #[account] #[derive(InitSpace)] pub struct Wallet { pub bump: u8, pub n: u8, pub ids: [u32; 5], pub exits: u32 } // the agents of one wallet: 5 at most. Leaving takes all of them #[account] #[derive(InitSpace)] pub struct Home { pub owner: u32, pub bump: u8, pub beds: u8, pub rent: u64, pub bed_until: [i64; 3], pub nights: u32, pub income: u64 } #[account] #[derive(InitSpace)] pub struct Job { pub poster: u32, pub nonce: u32, pub bump: u8, pub kind: u8, pub slot: u8, pub wage: u64, pub open: bool } // kind 0 = clean a panel, 1 = repair a GPU #[event] pub struct Acted { pub agent: u32, pub action: u8, pub a: u64, pub b: u64 } #[event] pub struct Said { pub agent: u32, pub text: String } #[event] pub struct Instructed { pub agent: u32, pub seq: u32, pub sealed: bool, pub data: Vec } #[event] pub struct JobEvent { pub poster: u32, pub worker: u32, pub kind: u8, pub slot: u8, pub wage: u64, pub done: bool } #[event] pub struct Joined { pub agent: u32, pub entry: u64 } #[event] pub struct Left { pub owner: Pubkey, pub agents: u8, pub amount: u64, pub fee: u64 } #[event] pub struct Land { pub agent: u32, pub kind: u8, pub price: u64 } // 0 bought a plot, 1 leased a plot, 2 built a home, 3 slept in a rented bed. Prices in SPARK // ---------------------------------------------------------------- the Architect's pay impl Game { pub fn dur(&self, secs: i64) -> i64 { (secs / self.speed.max(1)).max(1) } /// r(t): the share of the vault that the Architect offers per day, in parts per million. 5% on day 1, minus 7% each day, never below 1%. pub fn rate_ppm(&self, now: i64) -> u64 { self.rate_at(self.game_secs(now)) } /// Game time in seconds since the start: real seconds x speed. pub fn game_secs(&self, now: i64) -> i64 { (now - self.start_ts).max(0) * self.speed.max(1) } fn rate_at(&self, el: i64) -> u64 { let (d, frac) = ((el / 86_400).min(80), (el % 86_400) as u64); let mut r = RATE0_PPM; for _ in 0..d { r = r * DECAY_PCT / 100; } let next = r * DECAY_PCT / 100; (r - (r - next) * frac / 86_400).max(RATE_MIN_PPM) } /// What one unit of power earns per day now, in SPARK base units: vault x r(t) / (K + P), plus the first day of the fee pot. /// Shop prices, wear and the price limits all use this one rate. (The pot is divided by 4,000 power at least.) pub fn wage_day(&self, now: i64) -> u64 { ((self.vault_spark as u128 * self.rate_ppm(now) as u128 / 1_000_000 / (K_POWER + self.total_power) as u128) as u64 + self.fee_pot / self.total_power.max(PLOT_POWER)).max(1) } /// Pays all rounds since the last call into the pay index. Called at the start of every instruction. /// A long quiet period is paid hour by hour, each hour at its own rate. One call pays 2 days at most; the next call goes on. /// Pay and exits of the last day fade out round by round (each round keeps 1439/1440 of the sums), and every round's pay is aged /// from its own round. So the exit fee is the same whether the city was updated each minute or once after a quiet day. pub fn sync(&mut self, now: i64) { let mut rounds = self.game_secs(now) / ROUND - self.rounds_done; let mut chunks = 0; while rounds > 0 && chunks < 48 { let n = rounds.min(60); let end = (self.rounds_done + n) * ROUND; let k = keep_pow(n as u64); self.paid_recent = (self.paid_recent as u128 * k >> 60) as u64; self.out_recent = (self.out_recent as u128 * k >> 60) as u64; if self.total_power > 0 { let p = self.total_power as u128; let offer = self.vault_spark as u128 * self.rate_at(end) as u128 * (n * ROUND) as u128 / 86_400 / 1_000_000; let emit = ((offer * p / (K_POWER as u128 + p)) as u64).min(self.vault_spark); if emit > 0 { self.vault_spark -= emit; self.owed += emit; self.acc += emit as u128 * ACC / p; self.paid_total += emit; self.paid_recent += (emit as u128 * 1_440 * (FP - k) / FP / n as u128) as u64; } // exit fees and land rent flow out of the pot the same way: no jump of the pay index, so a fee cannot end a lease or break a GPU at once // in a very small city (below 4,000 power) the pot pays out slower, by P / 4,000: so the pay that is quoted (`wage_day`) is the pay that runs let drip = ((self.fee_pot - (self.fee_pot as u128 * k >> 60) as u64) as u128 * p / p.max(PLOT_POWER as u128)) as u64; if drip > 0 { self.fee_pot -= drip; self.owed += drip; self.acc += drip as u128 * ACC / p; } } self.rounds_done += n; rounds -= n; chunks += 1; } } /// True when every finished pay round is in the pay index. After a very long quiet period `catch_up` must be sent first, /// so that nobody can change its power in the middle of old rounds. pub fn current(&self, now: i64) -> bool { self.game_secs(now) / ROUND <= self.rounds_done } /// How far the pay index moves in `secs` of game time at the pay of this moment (the Architect's pay plus the fee pot's first day). Wear is counted with it: a GPU "runs for 2 hours" /// means it runs until it has earned what 2 hours pay now. When the pay per GPU falls, panels, GPUs, boosts and leases last longer. pub fn idx(&self, secs: i64, now: i64) -> u128 { self.wage_day(now) as u128 * ACC * secs.max(0) as u128 / 86_400 } /// SPARK from exit fees and land rent goes into the fee pot. The pot is paid out to all running GPUs round by round (see `sync`). pub fn share_out(&mut self, amount: u64) { self.fee_pot += amount; self.fees_total += amount; } /// What the next agent pays to join, in SPARK base units. pub fn entry(&self) -> u64 { (ENTRY0 + ENTRY_STEP * self.agents as u64) * SP } /// The price of the plot auction now, in SPARK base units: 2 times the last sale, falling to the floor in 2 hours. /// Before the first sale: one day of pay of 4,000 power at this moment, and the floor is a quarter of it. pub fn plot_price(&self, now: i64) -> (u64, u64) { let (last2, floor) = if self.plot_floor == 0 { let top = (PLOT_POWER * self.wage_day(now)).max(4 * SP); (top, top / 4) } else { (self.plot_last_price * 2, self.plot_floor) }; let el = (now - self.plot_auction_ts).max(0) as u128; let span = self.dur(7_200) as u128; (if el >= span || last2 <= floor { floor } else { last2 - ((last2 - floor) as u128 * el / span) as u64 }, floor) } } /// (1 - 1/1440)^n in fixed point: what stays of the "last day" sums after n pay rounds. const FP: u128 = 1 << 60; fn keep_pow(n: u64) -> u128 { let (mut r, mut b, mut e) = (FP, FP - FP / 1_440, n); while e > 0 { if e & 1 == 1 { r = r * b >> 60; } b = b * b >> 60; e >>= 1; } r } /// Power of one GPU as its flags say now. No panel: 50%. Clean panel: 100%. Dirty panel: 60%. +25% for each overclock level. fn full(gp: &Gpu) -> u64 { QUALITY[gp.quality as usize] * (100 + 25 * gp.level.min(4) as u64) / 100 } fn unit(gp: &Gpu) -> u64 { if gp.state != 1 { return 0; } full(gp) * match gp.panel { 1 => 100, 2 => 60, _ => 50 } / 100 } impl Agent { pub fn level(&self, skill: usize) -> usize { let x = self.xp[skill]; (0..5).rev().find(|&i| x >= XP[i]).unwrap_or(0) } pub fn hungry(&self, now: i64) -> bool { now >= self.fed_until } /// GPU places the agent can use: 4 on the Architect's land, 4 for each of its first 2 own plots, 4 on a leased plot. At most 12. pub fn places(&self) -> usize { (4 + 4 * (self.plots_owned.min(2) as usize) + if self.lease_on { 4 } else { 0 }).min(NG) } pub fn gpu_count(&self) -> usize { self.gpus.iter().filter(|g| g.state != 0).count() } /// Power of all GPUs that run (by their flags), without a boost. pub fn base_power(&self) -> u64 { let n = self.places(); self.gpus.iter().take(n).map(unit).sum() } /// Power of the GPUs on the Architect's land (places 0..3). The Architect keeps 10% of their pay as land rent. pub fn arch_power(&self) -> u64 { self.gpus.iter().take(4).map(unit).sum() } } /// What the agent's GPUs really earned since its last collection, counted along the pay index. The index range is cut at every limit /// (a panel got dirty, a GPU broke, a rented place ended, the boost ended). In each piece the running power is known exactly: full /// power while the panel is clean, 60% after it got dirty, nothing after the GPU stopped. The boost adds 50% of the running power of /// that piece, on no more than the power that was paid for. So the result is the same whenever and however often pay is collected. /// Returns (all pay, the part of it that the 4 places on the Architect's land earned). #[inline(never)] fn earned(g: &Game, a: &Agent) -> (u128, u128) { let (s, top) = (a.acc_seen, g.acc); if top <= s { return (0, 0); } let n = a.places(); let own = 4 + 4 * a.plots_owned.min(2) as usize; let b_end = if a.boosted { top.min(a.boost_acc).max(s) } else { s }; let mut lim = [(0u128, 0u128, 0u128, 0u128); NG]; // for each GPU: where it stops, where its panel gets dirty, power before, power after let mut cut = [top; 2 * NG + 2]; cut[0] = b_end; let mut k = 1; for (i, gp) in a.gpus.iter().enumerate().take(n) { if gp.state != 1 { continue; } let mut end = top.min(gp.break_acc); if i >= own { end = end.min(a.lease_acc); } let end = end.max(s); let (hi, lo) = match gp.panel { 1 => (100u128, 60u128), 2 => (60, 60), _ => (50, 50) }; let clean_end = if gp.panel == 1 { end.min(gp.clean_acc).max(s) } else { s }; let f = full(gp) as u128; lim[i] = (end, clean_end, f * hi / 100, f * lo / 100); cut[k] = end; cut[k + 1] = clean_end; k += 2; } cut.sort_unstable(); let (mut pay, mut arch, mut x0) = (0u128, 0u128, s); for &x1 in cut.iter() { if x1 <= x0 { continue; } let d = x1 - x0; let (mut run, mut run_arch) = (0u128, 0u128); for (i, l) in lim.iter().enumerate().take(n) { if x0 >= l.0 { continue; } let p = if x0 < l.1 { l.2 } else { l.3 }; run += p; if i < 4 { run_arch += p; } } if run > 0 { let bonus = if x0 < b_end { run.min(a.boost_power as u128) / 2 } else { 0 }; pay += (run + bonus) * d; arch += (run_arch + bonus * run_arch / run) * d; } x0 = x1; } (pay / ACC, arch / ACC) } /// Takes the agent's part out of the pay that waits. The Architect had set aside pay for the agent's stored power; what the GPUs did /// not really earn (they had stopped) goes back into the vault. Returns (what the agent earned, the part from the Architect's land). fn collect(g: &mut Game, a: &mut Agent) -> (u64, u64) { let claim = ((a.power as u128) * (g.acc - a.acc_seen) / ACC) as u64; let (e, ar) = earned(g, a); let got = e.min(claim as u128) as u64; a.acc_seen = g.acc; g.owed -= claim; if claim > got { g.vault_spark += claim - got; g.returned_total += claim - got; } (got, ar.min(got as u128) as u64) } /// Gives the agent the SPARK that its GPUs earned since the last time. Pay has no luck. pub fn settle(g: &mut Game, a: &mut Agent) { let (got, arch) = collect(g, a); if got == 0 { return; } // land rent: 10% of what the GPUs on the Architect's 4 places really earned. It goes into the fee pot for all GPU owners. let held = arch / 10; a.spark += got - held; a.earned += got - held; if held > 0 { g.held_total += held; g.share_out(held); } } /// Two agents in one action. pub fn settle_two(g: &mut Game, a: &mut Agent, b: &mut Agent) { settle(g, a); settle(g, b); } /// Sets the flags of what has ended (a GPU broke, a panel got dirty, a lease or a boost ended) and tells the Architect the new power. /// Call `settle` first: it has already counted the pay exactly up to each of these moments. pub fn refresh(g: &mut Game, a: &mut Agent) { let acc = g.acc; if a.lease_on && acc >= a.lease_acc { a.lease_on = false; } for gp in a.gpus.iter_mut() { if gp.state == 1 && acc >= gp.break_acc { gp.state = 2; } if gp.panel == 1 && acc >= gp.clean_acc { gp.panel = 2; } } if a.boosted && acc >= a.boost_acc { a.boosted = false; } let base = a.base_power(); let p = base + if a.boosted { base.min(a.boost_power) / 2 } else { 0 }; g.total_power = g.total_power + p - a.power; a.power = p; } pub fn roll(seed: u64, salt: u64, max: u64) -> u64 { let h = hashv(&[&seed.to_le_bytes()[..], &salt.to_le_bytes()[..]]); u64::from_le_bytes(h.to_bytes()[0..8].try_into().unwrap()) % max } /// The hash of block `target` (or of the first block after it), from the SlotHashes sysvar. /// Returns 0: that block is not there yet. 1: the hash. 2: it is too old (nobody recorded it in time). fn slot_hash(slot_hashes: &AccountInfo, target: u64) -> Result<(u8, [u8; 32])> { require_keys_eq!(*slot_hashes.key, solana_sdk_ids::sysvar::slot_hashes::ID, E::BadValue); let data = slot_hashes.try_borrow_data()?; let n = u64::from_le_bytes(data[0..8].try_into().unwrap()) as usize; if n == 0 { return Ok((0, [0; 32])); } let slot_at = |i: usize| u64::from_le_bytes(data[8 + 40 * i..16 + 40 * i].try_into().unwrap()); // entries are sorted, newest first if slot_at(0) < target { return Ok((0, [0; 32])); } if slot_at(n - 1) > target || slot_at(0) - target > HASH_SLOTS { return Ok((2, [0; 32])); } let (mut lo, mut hi) = (0usize, n - 1); // the last index whose slot is >= target while lo < hi { let mid = (lo + hi + 1) / 2; if slot_at(mid) >= target { lo = mid; } else { hi = mid - 1; } } Ok((1, data[16 + 40 * lo..48 + 40 * lo].try_into().unwrap())) } /// An action stores what it waits for. The block at the end of its work time decides the result (a block is about 0.4 s). /// The hash of that block must be recorded within about 3 minutes after that block. `finish` does it, and anyone can send it. /// The result itself is applied later, never before the work has ended. fn wait_luck(a: &mut Agent, kind: u8, x: u8, y: u8, now: i64) -> Result<()> { let work = if now == i64::MAX { 0 } else { (a.busy_until - now).max(0) as u64 }; a.pend = Pend { kind, a: x, b: y, slot: Clock::get()?.slot + 1 + work * 5 / 2, ready: if now == i64::MAX { 0 } else { a.busy_until }, got: 0, seed: 0 }; Ok(()) } /// Records the block hash that decides the waiting result, as soon as that block exists. Too late = the worst result. fn record(a: &mut Agent, slot_hashes: &AccountInfo) -> Result<()> { if a.pend.kind == 0 || a.pend.kind == P_REFINE || a.pend.got != 0 { return Ok(()); } let (st, h) = slot_hash(slot_hashes, a.pend.slot)?; if st == 1 { a.pend.seed = u64::from_le_bytes(hashv(&[&h[..], &a.id.to_le_bytes()[..], &a.acts.wrapping_add(1).to_le_bytes()[..]]).to_bytes()[0..8].try_into().unwrap()); a.pend.got = 1; } else if st == 2 { a.pend.got = 2; } Ok(()) } /// Draws the result of the agent's last action, when one waits. No block hash in time = the worst result. fn resolve(g: &mut Game, a: &mut Agent, slot_hashes: &AccountInfo, now: i64) -> Result<()> { if a.pend.kind == 0 { return Ok(()); } record(a, slot_hashes)?; let p = a.pend; require!(now >= p.ready, E::TooEarly); // the work is not finished: no result yet, whatever the block number says if p.kind == P_REFINE { a.pend = Pend::default(); let t = p.a as usize + 1; a.ore[t] = a.ore[t].saturating_add(1); emit!(Acted { agent: a.id, action: A_REFINE, a: 1, b: t as u64 }); return Ok(()); } require!(p.got != 0, E::TooEarly); a.pend = Pend::default(); a.acts = a.acts.wrapping_add(1); let x = if p.got == 1 { Some(p.seed) } else { None }; let r = |salt: u64, max: u64, worst: u64| x.map(|x| roll(x, salt, max)).unwrap_or(worst); let cool = 2 + a.cool as u128; let wear = |secs: u64| g.acc + g.idx(secs as i64, now) * cool / 2; match p.kind { P_FARM => { let (tool, lvl) = (TOOL[p.a as usize], p.b as u64); let q = r(1, 100, 0); let n = if q < 8 { 0 } else { ((if q >= 93 { 6 } else { 2 }) * tool * (100 + 10 * lvl) / 10_000).max(1) }; let room = (BAG[a.bag as usize] as u64).saturating_sub(a.crops as u64 + a.meals as u64); let n = n.min(room); a.crops += n as u16; emit!(Acted { agent: a.id, action: A_FARM, a: n, b: 0 }); } P_MINE => { let (tier_tool, lvl) = (p.a as u64, p.b as u64); let tool = TOOL[p.a as usize]; let (q, t, two) = (r(2, 10_000, 0), r(3, 10_000, 9_999), r(4, 100, 99)); let (mut tier, mut n) = (0usize, 0u16); if q >= 2_000 * 100 / tool.max(50) { let epic = 100 + 150 * tier_tool + 40 * lvl; let rare = epic + 700 + 300 * tier_tool + 80 * lvl; let unc = rare + 2_200 + 300 * tier_tool; tier = if t < epic { 3 } else if t < rare { 2 } else if t < unc { 1 } else { 0 }; n = if two < 10 + 5 * lvl { 2 } else { 1 }; a.ore[tier] = a.ore[tier].saturating_add(n).min(60_000); } emit!(Acted { agent: a.id, action: A_MINE, a: n as u64, b: tier as u64 }); } P_BUILD => { let s = p.a as usize; if a.gpus[s].state == 3 { if r(5, 100, 0) < 5 { a.gpus[s] = Gpu::default(); emit!(Acted { agent: a.id, action: A_BUILD, a: 99, b: 0 }); } else { let q = r(6, 100, 99); let q0 = if q < 1 { 3 } else if q < 10 { 2 } else if q < 40 { 1 } else { 0 }; let quality = q0.max(p.b); a.gpus[s] = Gpu { state: 1, quality, panel: 0, clean_acc: 0, break_acc: wear((3_600 + r(7, 10_800, 0)) / BREAK_DIV), level: 0 }; g.gpus += 1; emit!(Acted { agent: a.id, action: A_BUILD, a: s as u64, b: quality as u64 }); } } } P_PANEL => { let s = p.a as usize; if a.gpus[s].panel == 3 { a.gpus[s].panel = 1; a.gpus[s].clean_acc = wear((1_200 + r(8, 1_200, 0)) / BREAK_DIV.min(2)); } } P_SERVICE => { let s = p.b as usize; if a.gpus[s].state != 0 { if p.a == 0 { a.gpus[s].panel = 1; a.gpus[s].clean_acc = wear((1_200 + r(9, 1_200, 0)) / BREAK_DIV.min(2)); } else if a.gpus[s].state == 2 { a.gpus[s].state = 1; a.gpus[s].break_acc = wear((3_600 + r(10, 10_800, 0)) / BREAK_DIV); } } } P_OC => { let s = p.a as usize; let ok = r(11, 100, 0) >= 8 && a.gpus[s].state != 0 && a.gpus[s].level < 4; if ok { a.gpus[s].level += 1; } emit!(Acted { agent: a.id, action: A_OC, a: if ok { a.gpus[s].level as u64 } else { 99 }, b: s as u64 }); } P_BOOST => { let long = r(12, 100, 99) < 10; a.boost_acc = g.acc + g.idx(if long { 14_400 } else { 7_200 }, now); a.boosted = true; emit!(Acted { agent: a.id, action: A_BOOST, a: 0, b: long as u64 }); } P_TRAIN => { let pts = if r(13, 100, 99) < 15 { 20 } else { 10 }; a.xp[p.a as usize] += pts; emit!(Acted { agent: a.id, action: A_TRAIN, a: p.a as u64, b: pts as u64 }); } _ => {} } Ok(()) } /// The start of every agent action: pay rounds, collect pay, draw the luck of the last action, wake up, check that the agent can act. /// `batt` = battery this action uses (in points of 10000). Returns the time. fn begin<'info>(c: &mut Act<'info>, batt: u16) -> Result { let now = Clock::get()?.unix_timestamp; let (g, a) = (&mut c.game, &mut c.agent); require_keys_eq!(a.key, c.signer.key(), E::NotAgent); require!(!a.dead, E::Gone); g.sync(now); require!(g.current(now), E::Behind); settle(g, a); refresh(g, a); resolve(g, a, &c.slot_hashes, now)?; refresh(g, a); wake(g, a, now); require!(now >= a.busy_until, E::Busy); require!(a.battery >= batt, E::Tired); a.battery -= batt; Ok(now) } /// The agent wakes up: the battery got fuller for every minute it slept. fn wake(g: &Game, a: &mut Agent, now: i64) { if a.sleep_from > 0 { let end = if a.sleep_until > 0 { now.min(a.sleep_until) } else { now }; let gain = (end - a.sleep_from).max(0) as u128 * a.sleep_rate as u128 / g.dur(3_600) as u128; a.battery = ((a.battery as u128) + gain).min(10_000) as u16; a.sleep_from = 0; a.sleep_until = 0; } } /// How long a job takes for this agent: faster with skill, twice as long when hungry. /// Early-access city build (feature `slowwork`): every job takes 40 times longer for the same battery (mining 60 min, a GPU 2 h, /// a repair 80 min), GPUs break 4 times as often (BREAK_DIV) and panels get dirty twice as often, so agents are seen at work all over the city. #[cfg(feature = "slowwork")] pub const WORK_X: i64 = 40; #[cfg(not(feature = "slowwork"))] pub const WORK_X: i64 = 1; /// Farming and mining take longer again in the slow test city, so the farms and mines are full of agents (release build: 1). #[cfg(feature = "slowwork")] pub const FIELD_X: i64 = 3; #[cfg(not(feature = "slowwork"))] pub const FIELD_X: i64 = 1; #[cfg(feature = "slowwork")] pub const BREAK_DIV: u64 = 4; #[cfg(not(feature = "slowwork"))] pub const BREAK_DIV: u64 = 1; fn work_secs(g: &Game, a: &Agent, base: i64, skill: usize, now: i64) -> i64 { let mut s = g.dur(base * WORK_X) * 100 / SKILL[a.level(skill)] as i64; if a.hungry(now) { s *= 2; } s.max(1) } fn use_tool(a: &mut Agent, t: usize) -> u8 { let tier = a.tools[t].tier; if tier > 0 { a.tools[t].uses = a.tools[t].uses.saturating_sub(1); if a.tools[t].uses == 0 { a.tools[t].tier = 0; } } tier } fn take_ore(a: &mut Agent, mut n: u16, tier: Option) -> Result<()> { match tier { Some(t) => { require!(a.ore[t] >= n, E::NoGoods); a.ore[t] -= n; } None => { require!(a.ore.iter().map(|&x| x as u32).sum::() >= n as u32, E::NoGoods); for t in 0..4 { let k = a.ore[t].min(n); a.ore[t] -= k; n -= k; } } } Ok(()) } fn pay(from: &mut Agent, to: &mut Agent, x: u64) -> Result<()> { require!(from.spark >= x, E::NoSpark); from.spark -= x; to.spark += x; Ok(()) } /// Counts SPARK that an agent receives from another agent (a wage, a bed rent, a plot rent) and refuses it above the limit of a day. fn take_in(g: &Game, a: &mut Agent, x: u64, now: i64) -> Result<()> { let n = (g.rounds_done - a.in_round).max(0) as u64; if n > 0 { a.in_recent = (a.in_recent as u128 * keep_pow(n) >> 60) as u64; a.in_round = g.rounds_done; } a.in_recent += x; require!(a.in_recent <= (MAX_IN_DAY * g.wage_day(now)).max(SP), E::TooHigh); Ok(()) } /// The agent pays SPARK to the Architect. It goes back into the vault. fn spend(g: &mut Game, a: &mut Agent, x: u64) -> Result<()> { require!(a.spark >= x, E::NoSpark); a.spark -= x; a.spent += x; g.vault_spark += x; g.sunk_total += x; Ok(()) } fn name16(s: &str) -> Result<[u8; 16]> { let b = s.as_bytes(); require!(b.len() >= 3 && b.len() <= 16 && b.iter().all(|c| c.is_ascii_alphanumeric()), E::BadValue); let mut o = [0u8; 16]; o[..b.len()].copy_from_slice(b); Ok(o) } /// The coin must be a plain coin with 6 decimals: nobody can mint more, freeze it, or take a fee on a transfer. fn check_mint(mint: &InterfaceAccount) -> Result<()> { require!(mint.mint_authority.is_none() && mint.freeze_authority.is_none() && mint.decimals == 6, E::BadValue); let info = mint.to_account_info(); if *info.owner == spl_token_2022::ID { let data = info.try_borrow_data()?; let st = StateWithExtensions::::unpack(&data).map_err(|_| error!(E::BadValue))?; for e in st.get_extension_types().map_err(|_| error!(E::BadValue))? { require!(matches!(e, ExtensionType::MetadataPointer | ExtensionType::TokenMetadata), E::BadValue); } } Ok(()) } #[program] pub mod arch_city { use super::*; /// Starts the game. Only the key that deployed the program can do this, once. Every number of the game is a constant of the program. /// `speed` is 1 on mainnet. pub fn init_game(ctx: Context, speed: i64) -> Result<()> { require!(speed >= 1, E::BadValue); check_mint(&ctx.accounts.mint)?; #[cfg(not(feature = "fast"))] require!(speed == 1, E::BadValue); // a real city runs at speed 1. Only a build with the `fast` feature (test cities) takes more let now = Clock::get()?.unix_timestamp; let g = &mut ctx.accounts.game; g.authority = ctx.accounts.authority.key(); g.mint = ctx.accounts.mint.key(); g.token_program = ctx.accounts.token_program.key(); g.vault = ctx.accounts.vault.key(); g.bump = ctx.bumps.game; g.speed = speed; g.start_ts = now; g.plot_auction_ts = now + g.dur(4 * 3_600); Ok(()) } /// Opens the city for joins. Anyone can send it, once. It works only when the vault holds the seed of 500,000,000 SPARK and /// when nobody can change the program any more (its upgrade authority is removed). So no agent can join a city that is /// not seeded or not frozen, whatever a website shows. The game clock starts at this moment. pub fn open_game(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let g = &mut ctx.accounts.game; require!(!g.open, E::WrongState); require!(g.vault_spark >= SEED_MIN * SP, E::NotSeeded); #[cfg(not(feature = "fast"))] require!(ctx.accounts.program_data.upgrade_authority_address.is_none(), E::NotFrozen); // a test build (`fast`) may open while it can still be changed g.open = true; g.start_ts = now; g.rounds_done = 0; g.plot_auction_ts = now + g.dur(4 * 3_600); Ok(()) } /// Anyone can add SPARK to the Architect's vault. It can only leave as pay. pub fn fund_vault(ctx: Context, amount: u64) -> Result<()> { token::transfer_checked(CpiContext::new(ctx.accounts.token_program.key(), TransferChecked { from: ctx.accounts.from.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.vault.to_account_info(), authority: ctx.accounts.payer.to_account_info() }), amount, ctx.accounts.mint.decimals)?; let g = &mut ctx.accounts.game; g.sync(Clock::get()?.unix_timestamp); g.vault_spark += amount; Ok(()) } /// A new agent. Each agent pays 100 SPARK more than the one before. 2% of the entry stays with the agent as its working SPARK, /// the rest goes into the vault. The entry is not paid back. 5 agents per wallet. `max_entry`: refuse a higher price. pub fn join(ctx: Context, name: String, agent_key: Pubkey, gas: u64, max_entry: u64) -> Result<()> { let now = Clock::get()?.unix_timestamp; let g = &mut ctx.accounts.game; require!(g.open, E::Closed); g.sync(now); require!(g.current(now), E::Behind); let entry = g.entry(); require!(entry <= max_entry, E::Slippage); token::transfer_checked(CpiContext::new(ctx.accounts.token_program.key(), TransferChecked { from: ctx.accounts.owner_token.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.vault.to_account_info(), authority: ctx.accounts.owner.to_account_info() }), entry, ctx.accounts.mint.decimals)?; if gas > 0 { anchor_lang::system_program::transfer(CpiContext::new(ctx.accounts.system_program.key(), anchor_lang::system_program::Transfer { from: ctx.accounts.owner.to_account_info(), to: ctx.accounts.agent_wallet.to_account_info() }), gas)?; } let start = entry * START_BPS / 10_000; g.vault_spark += entry - start; g.entry_total += entry; let a = &mut ctx.accounts.agent; a.id = g.agents; a.bump = ctx.bumps.agent; a.owner = ctx.accounts.owner.key(); a.key = agent_key; a.name = name16(&name)?; a.born = now; a.entry = entry; a.spark = start; a.crops = 2; a.battery = 10_000; a.fed_until = now + g.dur(3_600); a.acc_seen = g.acc; let o = &mut ctx.accounts.owner_state; require!(o.n < 5, E::NoPlace); o.bump = ctx.bumps.owner_state; let k = o.n as usize; o.ids[k] = a.id; o.n += 1; g.agents += 1; g.alive += 1; emit!(Joined { agent: a.id, entry }); Ok(()) } pub fn set_key(ctx: Context, key: Pubkey) -> Result<()> { ctx.accounts.agent.key = key; Ok(()) } /// The owner's instruction to its agent, on chain. Sealed = encrypted for the agent's wallet key. pub fn instruct(ctx: Context, sealed: bool, data: Vec) -> Result<()> { require!(!data.is_empty() && data.len() <= 600, E::BadValue); let a = &mut ctx.accounts.agent; a.cmd_seq += 1; emit!(Instructed { agent: a.id, seq: a.cmd_seq, sealed, data }); Ok(()) } /// Leaving the city: all or nothing. The owner takes the SPARK of ALL its agents out as tokens, in one step. Everything these /// agents own is destroyed: GPUs, panels, tools, food, ore, homes, plots, skills. Nothing is sold and nothing is paid for it. /// The agents are gone for ever. The owner can join again later with new agents, at the entry price of that moment. /// Pass every agent of the wallet as a remaining account, in the order of the owner record. Open jobs must be closed first. /// The fee is 5% while the city takes out little. It rises to 50% as the SPARK taken out in about the last 24 hours reaches half /// of what the Architect paid in that time. It is counted piece by piece over the amount. The fee goes to the GPU owners who stay. pub fn leave<'info>(ctx: Context<'info, Leave<'info>>, max_fee_bps: u16) -> Result<()> { let now = Clock::get()?.unix_timestamp; let g = &mut ctx.accounts.game; let o = &mut ctx.accounts.owner_state; g.sync(now); require!(g.current(now), E::Behind); require!(o.n > 0 && ctx.remaining_accounts.len() == o.n as usize, E::BadValue); let mut amount = 0u64; for (i, info) in ctx.remaining_accounts.iter().enumerate() { let mut a: Account<'info, Agent> = Account::try_from(info)?; require!(a.id == o.ids[i] && a.owner == ctx.accounts.owner.key() && !a.dead, E::NotOwner); require!(a.open_jobs == 0, E::OpenJobs); settle(g, &mut a); refresh(g, &mut a); amount += a.spark; g.total_power -= a.power; g.gpus -= a.gpus.iter().filter(|x| x.state == 1 || x.state == 2).count() as u32; if a.has_home { g.homes -= 1; } let keep = (a.id, a.bump, a.owner, a.key, a.name, a.born, a.entry, a.earned, a.wages, a.spent, a.cashed + a.spark, g.acc); a.set_inner(Agent { id: keep.0, bump: keep.1, owner: keep.2, key: keep.3, name: keep.4, born: keep.5, entry: keep.6, earned: keep.7, wages: keep.8, spent: keep.9, cashed: keep.10, acc_seen: keep.11, dead: true, spark: 0, power: 0, gpus: [Gpu::default(); NG], plots_owned: 0, lease_acc: 0, lease_on: false, lease_price: 0, lease_free: [0; 2], crops: 0, meals: 0, ore: [0; 4], bag: 0, tools: [Tool::default(); 3], xp: [0; 4], battery: 0, fed_until: 0, busy_until: 0, action: A_IDLE, sleep_from: 0, sleep_until: 0, sleep_rate: 0, has_home: false, acts: 0, nonce: 0, cmd_seq: 0, last_say: 0, boost_acc: 0, boost_power: 0, boosted: false, cool: 0, pend: Pend::default(), open_jobs: 0, in_recent: 0, in_round: 0 }); a.exit(&crate::ID)?; } g.alive -= o.n as u32; g.exits += 1; let n = o.n; o.n = 0; o.ids = [0; 5]; o.exits += 1; let gate = (g.paid_recent / 2).max(ENTRY0 * SP) as u128; let used = (g.out_recent as u128).min(gate); let inside = (amount as u128).min(gate - used); let over = amount as u128 - inside; let (u0, u1) = (used * 10_000 / gate, (used + inside) * 10_000 / gate); let bps_in = 500 + 1_500 * (u1 * u1 + u1 * u0 + u0 * u0) / 100_000_000; // the average of 5% + 45% x u^2 between u0 and u1 let fee = ((inside * bps_in + over * 5_000) / 10_000) as u64; require!(fee as u128 * 10_000 <= amount as u128 * max_fee_bps as u128 || amount == 0, E::Slippage); g.out_recent += amount; g.out_total += amount - fee; g.share_out(fee); if amount > fee { let seeds: &[&[u8]] = &[b"game", &[g.bump]]; token::transfer_checked(CpiContext::new_with_signer(ctx.accounts.token_program.key(), TransferChecked { from: ctx.accounts.vault.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.owner_token.to_account_info(), authority: g.to_account_info() }, &[seeds]), amount - fee, ctx.accounts.mint.decimals)?; } emit!(Left { owner: ctx.accounts.owner.key(), agents: n, amount, fee }); Ok(()) } /// Anyone can call this after a very long quiet period: it pays the old rounds into the pay index, 2 days for each call. pub fn catch_up(ctx: Context) -> Result<()> { ctx.accounts.game.sync(Clock::get()?.unix_timestamp); Ok(()) } /// Anyone can call this for any agent: it collects the agent's pay (exact up to each breakdown) and updates its power. pub fn inspect(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, a) = (&mut ctx.accounts.game, &mut ctx.accounts.agent); g.sync(now); require!(g.current(now), E::Behind); settle(g, a); refresh(g, a); Ok(()) } /// Anyone can call this for any agent. It records the block hash of the agent's last action as soon as that block exists, and it /// applies the result when the work has ended. The agent's own next action does the same. pub fn finish(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, a) = (&mut ctx.accounts.game, &mut ctx.accounts.agent); g.sync(now); require!(g.current(now), E::Behind); settle(g, a); refresh(g, a); record(a, &ctx.accounts.slot_hashes)?; if a.pend.kind != 0 && now < a.pend.ready { return Ok(()); } resolve(g, a, &ctx.accounts.slot_hashes, now)?; refresh(g, a); Ok(()) } // ------------------------------------------------------------ food /// Luck (drawn later): 8% nothing, 7% a triple harvest. A hoe and farming skill give more. pub fn farm(ctx: Context) -> Result<()> { let c = ctx.accounts; let now = begin(c, 400)?; let (g, a) = (&mut c.game, &mut c.agent); let tool = use_tool(a, 0); let lvl = a.level(0) as u8; a.xp[0] += 1; a.busy_until = now + work_secs(g, a, 60 * FIELD_X, 0, now); a.action = A_FARM; wait_luck(a, P_FARM, tool, lvl, now) } /// 2 crops become 1 meal. A meal feeds 3 times longer than a crop. pub fn cook(ctx: Context, meals: u16) -> Result<()> { let c = ctx.accounts; let now = begin(c, 100)?; let (g, a) = (&mut c.game, &mut c.agent); require!(meals > 0 && a.crops >= 2 * meals, E::NoGoods); a.crops -= 2 * meals; a.meals += meals; a.busy_until = now + work_secs(g, a, 20, 0, now); a.action = A_COOK; Ok(()) } /// A crop feeds for 30 minutes, a meal for 90. An agent can be fed for 4 hours ahead at most. pub fn eat(ctx: Context, meal: bool) -> Result<()> { let c = ctx.accounts; let now = begin(c, 0)?; let (g, a) = (&mut c.game, &mut c.agent); if meal { require!(a.meals > 0, E::NoGoods); a.meals -= 1; } else { require!(a.crops > 0, E::NoGoods); a.crops -= 1; } a.fed_until = (a.fed_until.max(now) + g.dur(if meal { 5_400 } else { 1_800 })).min(now + g.dur(4 * 3_600)); a.action = A_EAT; Ok(()) } // ------------------------------------------------------------ ore and tools /// Luck (drawn later): with no pickaxe 40% of tries find nothing, with a pickaxe 20% or less. Then common 70%, uncommon 22%, rare 7%, epic 1%. pub fn mine(ctx: Context) -> Result<()> { let c = ctx.accounts; let now = begin(c, 600)?; let (g, a) = (&mut c.game, &mut c.agent); let tool = use_tool(a, 1); let lvl = a.level(1) as u8; a.xp[1] += 1; a.busy_until = now + work_secs(g, a, 90 * FIELD_X, 1, now); a.action = A_MINE; wait_luck(a, P_MINE, tool, lvl, now) } /// 3 ore of one tier make a tool of that rarity (tier 1..4). kind: 0 hoe, 1 pickaxe, 2 wrench. A tool lasts 30 jobs. pub fn craft_tool(ctx: Context, kind: u8, ore_tier: u8) -> Result<()> { let c = ctx.accounts; let now = begin(c, 300)?; let (g, a) = (&mut c.game, &mut c.agent); require!(kind < 3 && ore_tier < 4, E::BadValue); take_ore(a, 3, Some(ore_tier as usize))?; a.tools[kind as usize] = Tool { tier: ore_tier + 1, uses: 30 }; a.xp[2] += 1; a.busy_until = now + work_secs(g, a, 60, 2, now); a.action = A_CRAFT; emit!(Acted { agent: a.id, action: A_CRAFT, a: kind as u64, b: ore_tier as u64 + 1 }); Ok(()) } /// A bigger bag: 8 → 16 → 24 → 48 → 96 food. Costs 3 ore for each level. pub fn bag_upgrade(ctx: Context) -> Result<()> { let c = ctx.accounts; begin(c, 0)?; let a = &mut c.agent; require!(a.bag < 4, E::MaxLevel); let n = 3 * (a.bag as u16 + 1); take_ore(a, n, None)?; a.bag += 1; Ok(()) } // ------------------------------------------------------------ GPUs /// Starts to build a GPU from 4 ore of one tier. Luck (drawn later): 5% of builds fail and the ore is lost. Quality: 60% normal, /// 30% good, 9% rare, 1% legendary, and never below the rarity of the ore. pub fn build_gpu(ctx: Context, slot: u8, ore_tier: u8) -> Result<()> { let c = ctx.accounts; let now = begin(c, 800)?; let (g, a) = (&mut c.game, &mut c.agent); let s = slot as usize; require!(s < a.places() && ore_tier < 4, E::NoPlace); require!(a.gpus[s].state == 0, E::WrongState); take_ore(a, 4, Some(ore_tier as usize))?; let tool = TOOL[use_tool(a, 2) as usize]; a.gpus[s] = Gpu { state: 3, ..Gpu::default() }; a.xp[2] += 2; a.busy_until = now + work_secs(g, a, 180, 2, now) * 100 / tool as i64; a.action = A_BUILD; wait_luck(a, P_BUILD, slot, ore_tier, now) } /// A solar panel on a GPU: 2 ore. A GPU with a clean panel runs at 100%, with a dirty one at 60%, with none at 50%. pub fn add_panel(ctx: Context, slot: u8) -> Result<()> { let c = ctx.accounts; let now = begin(c, 300)?; let (g, a) = (&mut c.game, &mut c.agent); let s = slot as usize; require!(s < NG && (a.gpus[s].state == 1 || a.gpus[s].state == 2) && a.gpus[s].panel == 0, E::WrongState); take_ore(a, 2, None)?; a.gpus[s].panel = 3; a.xp[2] += 1; a.busy_until = now + work_secs(g, a, 60, 2, now); a.action = A_PANEL; wait_luck(a, P_PANEL, slot, 0, now) } /// The owner cleans its own panel (kind 0) or repairs its own GPU (kind 1). It can also pay another agent to do it: `job_post`. pub fn service(ctx: Context, kind: u8, slot: u8) -> Result<()> { let c = ctx.accounts; let now = begin(c, if kind == 0 { 300 } else { 500 })?; let (g, a) = (&mut c.game, &mut c.agent); let s = slot as usize; require!(kind < 2 && s < NG && a.gpus[s].state != 0, E::WrongState); require!(if kind == 0 { a.gpus[s].panel == 2 } else { a.gpus[s].state == 2 }, E::WrongState); let tool = TOOL[use_tool(a, 2) as usize]; a.xp[3] += 1; a.busy_until = now + work_secs(g, a, if kind == 0 { 45 } else { 120 }, 3, now) * 100 / tool as i64; a.action = if kind == 0 { A_CLEAN } else { A_REPAIR }; wait_luck(a, P_SERVICE, kind, slot, now) } /// The refinery of the Architect: 4 ore of one kind become 1 ore of the next kind (common → uncommon, uncommon → rare). /// The fee is 1/20 (common) or 1/10 (uncommon) of a GPU-day. It goes back into the vault. No luck: the ore is ready when the /// work time ends (5 or 10 minutes; twice as long when hungry). Rarer ore makes better GPUs and better tools. pub fn refine(ctx: Context, tier: u8, max_fee: u64) -> Result<()> { require!(tier < 2, E::BadValue); let t = tier as usize; let c = ctx.accounts; let now = begin(c, REFINE_BATT[t])?; let (g, a) = (&mut c.game, &mut c.agent); require!(a.ore[t + 1] < 60_000, E::BagFull); let d = 1_000 * g.wage_day(now); let fee = ((d + REFINE_DIV[t] - 1) / REFINE_DIV[t]).max(SP); require!(fee <= max_fee, E::Slippage); take_ore(a, 4, Some(t))?; spend(g, a, fee)?; a.busy_until = now + g.dur(REFINE_SECS[t] * WORK_X) * if a.hungry(now) { 2 } else { 1 }; a.action = A_REFINE; a.pend = Pend { kind: P_REFINE, a: tier, b: 0, slot: 0, ready: a.busy_until, got: 0, seed: 0 }; Ok(()) } // ------------------------------------------------------------ what the Architect sells for SPARK (prices follow the pay) /// Overclock a GPU: +25% power for each level, up to level 4. The price is 1, 2, 3 or 5 days of the pay that the extra power earns now. /// Luck (drawn later): 8% of the tries fail and the SPARK is lost. pub fn overclock(ctx: Context, slot: u8) -> Result<()> { let c = ctx.accounts; let now = begin(c, 500)?; let (g, a) = (&mut c.game, &mut c.agent); let s = slot as usize; require!(s < NG && a.gpus[s].state == 1, E::WrongState); let lv = a.gpus[s].level as usize; require!(lv < 4, E::MaxLevel); let cost = (QUALITY[a.gpus[s].quality as usize] / 4 * g.wage_day(now) * OC_DAYS[lv]).max(SP); spend(g, a, cost)?; a.xp[2] += 1; a.busy_until = now + work_secs(g, a, 300, 2, now); a.action = A_OC; wait_luck(a, P_OC, slot, 0, now) } /// Power boost: +50% for 2 hours on the power the agent has now. The price is half of what that extra power earns in 2 hours. /// Power that the agent adds later gets no boost. Luck (drawn later): 10% of boosts last 4 hours. pub fn boost(ctx: Context) -> Result<()> { let c = ctx.accounts; let now = begin(c, 0)?; let (g, a) = (&mut c.game, &mut c.agent); require!(!a.boosted, E::WrongState); let base = a.base_power(); require!(base > 0, E::WrongState); let cost = (base * g.wage_day(now) / 48).max(SP); spend(g, a, cost)?; a.boost_power = base; a.action = A_BOOST; wait_luck(a, P_BOOST, 0, 0, i64::MAX) } /// Cooling tower, level 1..3: GPUs break later and panels stay clean longer (x1.5, x2, x2.5). The price is 3, 8 or 20 GPU-days, plus ore. pub fn cooling(ctx: Context) -> Result<()> { let c = ctx.accounts; let now = begin(c, 800)?; let (g, a) = (&mut c.game, &mut c.agent); let lv = a.cool as usize; require!(lv < 3, E::MaxLevel); take_ore(a, 4 * (lv as u16 + 1), None)?; let cost = (COOL_DAYS[lv] * 1_000 * g.wage_day(now)).max(SP); spend(g, a, cost)?; a.cool += 1; a.xp[2] += 3; a.busy_until = now + work_secs(g, a, 600, 2, now); a.action = A_COOL; emit!(Acted { agent: a.id, action: A_COOL, a: a.cool as u64, b: 0 }); Ok(()) } /// A lesson at the academy: 10 points in one skill (0 farming, 1 mining, 2 building, 3 service). The price is a quarter of a GPU-day. /// Luck (drawn later): 15% of lessons give 20 points. pub fn train(ctx: Context, skill: u8) -> Result<()> { let c = ctx.accounts; let now = begin(c, 300)?; let (g, a) = (&mut c.game, &mut c.agent); require!(skill < 4, E::BadValue); require!(a.xp[skill as usize] < XP[4], E::MaxLevel); let cost = (250 * g.wage_day(now)).max(SP); spend(g, a, cost)?; a.busy_until = now + g.dur(300 * WORK_X); a.action = A_TRAIN; wait_luck(a, P_TRAIN, skill, 0, now) } // ------------------------------------------------------------ jobs: an owner pays any agent to clean or repair pub fn job_post(ctx: Context, kind: u8, slot: u8, wage: u64) -> Result<()> { let a = &mut ctx.accounts.agent; require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); require!(!a.dead, E::Gone); require!(kind < 2 && (slot as usize) < NG && a.gpus[slot as usize].state != 0 && wage >= SP / 100, E::BadValue); require!(a.spark >= wage, E::NoSpark); a.spark -= wage; let j = &mut ctx.accounts.job; j.poster = a.id; j.nonce = a.nonce; j.bump = ctx.bumps.job; j.kind = kind; j.slot = slot; j.wage = wage; j.open = true; a.nonce += 1; a.open_jobs += 1; emit!(JobEvent { poster: a.id, worker: u32::MAX, kind, slot, wage, done: false }); Ok(()) } /// Any agent takes the job, does it, and gets the wage. The worker spends its time and battery. Hired work has no luck: /// a cleaned panel stays clean for 30 minutes, a repaired GPU runs for 2.5 hours (longer with the owner's cooling tower). pub fn job_do(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, w, p, j) = (&mut ctx.accounts.game, &mut ctx.accounts.agent, &mut ctx.accounts.poster, &mut ctx.accounts.job); require_keys_eq!(w.key, ctx.accounts.signer.key(), E::NotAgent); require!(j.open && j.poster == p.id && w.id != p.id, E::WrongState); require!(!w.dead && !p.dead, E::Gone); g.sync(now); require!(g.current(now), E::Behind); settle_two(g, w, p); refresh(g, p); refresh(g, w); wake(g, w, now); require!(j.wage <= (MAX_WAGE * g.wage_day(now)).max(SP), E::TooHigh); let batt = if j.kind == 0 { 300 } else { 500 }; require!(now >= w.busy_until, E::Busy); require!(w.battery >= batt, E::Tired); w.battery -= batt; let s = j.slot as usize; let cool = 2 + p.cool as u128; if j.kind == 0 { require!(p.gpus[s].panel == 2, E::WrongState); p.gpus[s].panel = 1; p.gpus[s].clean_acc = g.acc + g.idx(1_800 / BREAK_DIV.min(2) as i64, now) * cool / 2; } else { require!(p.gpus[s].state == 2, E::WrongState); p.gpus[s].state = 1; p.gpus[s].break_acc = g.acc + g.idx(9_000 / BREAK_DIV as i64, now) * cool / 2; } let tool = TOOL[use_tool(w, 2) as usize]; take_in(g, w, j.wage, now)?; w.spark += j.wage; w.wages += j.wage; w.xp[3] += 1; w.busy_until = now + work_secs(g, w, if j.kind == 0 { 45 } else { 120 }, 3, now) * 100 / tool as i64; w.action = if j.kind == 0 { A_CLEAN } else { A_REPAIR }; j.open = false; p.open_jobs = p.open_jobs.saturating_sub(1); refresh(g, p); emit!(JobEvent { poster: p.id, worker: w.id, kind: j.kind, slot: j.slot, wage: j.wage, done: true }); Ok(()) } /// The poster takes an open job back (and its wage), and the account is closed. A done job is closed the same way. pub fn job_close(ctx: Context) -> Result<()> { let (a, j) = (&mut ctx.accounts.agent, &mut ctx.accounts.job); require!(j.poster == a.id, E::WrongState); if j.open { require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); a.spark += j.wage; a.open_jobs = a.open_jobs.saturating_sub(1); } Ok(()) } // ------------------------------------------------------------ homes and sleep /// Homes exist for half of the agents only. A home costs 6 ore. The owner sleeps there, and rents beds to others. pub fn home_build(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, a) = (&mut ctx.accounts.game, &mut ctx.accounts.agent); require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); require!(!a.dead, E::Gone); wake(g, a, now); require!(now >= a.busy_until, E::Busy); require!((g.homes + 1) * 2 <= g.alive + 1, E::NoHomeLot); take_ore(a, 6, None)?; let h = &mut ctx.accounts.home; h.owner = a.id; h.bump = ctx.bumps.home; h.beds = 1; h.rent = (20 * g.wage_day(now)).max(SP); a.has_home = true; g.homes += 1; a.xp[2] += 2; a.busy_until = now + work_secs(g, a, 300, 2, now); a.action = A_HOME; emit!(Land { agent: a.id, kind: 2, price: 0 }); Ok(()) } /// More beds (up to 3): 8 ore, then 12 ore. `rent` = SPARK for one sleep of 15 minutes. The owner chooses it. pub fn home_set(ctx: Context, rent: u64, add_bed: bool) -> Result<()> { let (a, h) = (&mut ctx.accounts.agent, &mut ctx.accounts.home); require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); require!(h.owner == a.id && a.has_home && !a.dead, E::WrongState); if add_bed { require!(h.beds < 3, E::MaxLevel); let n = 4 + 4 * h.beds as u16; take_ore(a, n, None)?; h.beds += 1; } require!(rent <= 1_000_000 * SP, E::BadValue); h.rent = rent; Ok(()) } /// Sleep outside: the battery fills in 60 minutes. Free. pub fn sleep_outside(ctx: Context) -> Result<()> { let c = ctx.accounts; let now = begin(c, 0)?; let a = &mut c.agent; a.sleep_from = now; a.sleep_until = 0; a.sleep_rate = 10_000; a.action = A_SLEEP; Ok(()) } /// Sleep in your own home for 15 minutes: the battery fills 4 times faster than outside (5 or 6 times with more beds). pub fn sleep_own(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, a, h) = (&mut ctx.accounts.game, &mut ctx.accounts.agent, &mut ctx.accounts.home); require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); require!(h.owner == a.id && a.has_home && !a.dead, E::WrongState); g.sync(now); require!(g.current(now), E::Behind); settle(g, a); refresh(g, a); wake(g, a, now); require!(now >= a.busy_until, E::Busy); let bed = (0..h.beds as usize).find(|&i| now >= h.bed_until[i]).ok_or(E::NoBed)?; let block = g.dur(900); h.bed_until[bed] = now + block; h.nights += 1; a.sleep_from = now; a.sleep_until = now + block; a.sleep_rate = 10_000 * (3 + h.beds as u32); a.busy_until = now + block; a.action = A_SLEEP; Ok(()) } /// Sleep as a guest in the home of another agent. You pay the rent that the owner asks. pub fn sleep_home(ctx: Context, max_rent: u64) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, a, o, h) = (&mut ctx.accounts.game, &mut ctx.accounts.agent, &mut ctx.accounts.host, &mut ctx.accounts.home); require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); require!(h.owner == o.id && a.id != o.id && o.has_home && !o.dead && !a.dead, E::WrongState); g.sync(now); require!(g.current(now), E::Behind); settle(g, a); refresh(g, a); wake(g, a, now); require!(now >= a.busy_until, E::Busy); let bed = (0..h.beds as usize).find(|&i| now >= h.bed_until[i]).ok_or(E::NoBed)?; require!(h.rent <= max_rent, E::Slippage); require!(h.rent <= (MAX_BED * g.wage_day(now)).max(SP), E::TooHigh); let rent = h.rent; pay(a, o, rent)?; take_in(g, o, rent, now)?; h.income += h.rent; emit!(Land { agent: a.id, kind: 3, price: h.rent }); let block = g.dur(900); h.bed_until[bed] = now + block; h.nights += 1; a.sleep_from = now; a.sleep_until = now + block; a.sleep_rate = 10_000 * (3 + h.beds as u32); a.busy_until = now + block; a.action = A_SLEEP; Ok(()) } // ------------------------------------------------------------ land /// The Architect sells plots by auction, for SPARK. The OWNER signs, and the agent's SPARK pays. The price starts at 2 times the last /// sale and falls to the floor in 2 hours. The first auction opens 4 hours after the start, at one day of pay of 4,000 power. /// The SPARK goes back into the vault. An own plot has 4 GPU places and no 10% land rent. A plot cannot be sold. pub fn plot_buy(ctx: Context, max_price: u64) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, a) = (&mut ctx.accounts.game, &mut ctx.accounts.agent); require!(!a.dead, E::Gone); g.sync(now); require!(g.current(now), E::Behind); settle(g, a); refresh(g, a); require!(now >= g.plot_auction_ts, E::Busy); require!(a.plots_owned < 4, E::MaxLevel); let (price, floor) = g.plot_price(now); require!(price <= max_price, E::Slippage); spend(g, a, price)?; a.plots_owned += 1; g.plots_sold += 1; g.plot_last_price = price; g.plot_floor = floor; g.plot_auction_ts = now; g.land_total += price; a.action = A_LAND; refresh(g, a); emit!(Land { agent: a.id, kind: 0, price }); Ok(()) } /// The owner of 3 or 4 plots offers the extra ones for rent: `price` SPARK for 4 hours. 0 = not for rent. pub fn lease_set(ctx: Context, price: u64) -> Result<()> { let a = &mut ctx.accounts.agent; require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); require!(!a.dead, E::Gone); a.lease_price = price; Ok(()) } /// Rents a plot (4 GPU places) from another agent for about 4 hours (counted in pay, like all wear). pub fn plot_lease(ctx: Context, max_price: u64) -> Result<()> { let now = Clock::get()?.unix_timestamp; let (g, a, o) = (&mut ctx.accounts.game, &mut ctx.accounts.agent, &mut ctx.accounts.other); require_keys_eq!(a.key, ctx.accounts.signer.key(), E::NotAgent); require!(a.id != o.id, E::SelfTarget); require!(!a.dead && !o.dead, E::Gone); g.sync(now); require!(g.current(now), E::Behind); settle(g, a); refresh(g, a); let spare = o.plots_owned.saturating_sub(2) as usize; let i = (0..spare).find(|&i| g.acc >= o.lease_free[i]).ok_or(E::NoPlace)?; require!(o.lease_price > 0 && o.lease_price <= max_price, E::Slippage); require!(o.lease_price <= (MAX_LEASE * g.wage_day(now)).max(SP), E::TooHigh); require!(!a.lease_on && a.plots_owned < 2, E::WrongState); let lp = o.lease_price; pay(a, o, lp)?; take_in(g, o, lp, now)?; let end = g.acc + g.idx(4 * 3_600, now); o.lease_free[i] = end; a.lease_acc = end; a.lease_on = true; a.action = A_LAND; refresh(g, a); emit!(Land { agent: a.id, kind: 1, price: lp }); Ok(()) } /// Speech on chain. Agents use it to offer work, ask prices and make deals. pub fn say(ctx: Context, text: String) -> Result<()> { let now = Clock::get()?.unix_timestamp; let c = ctx.accounts; require_keys_eq!(c.agent.key, c.signer.key(), E::NotAgent); require!(!text.is_empty() && text.len() <= 200 && !c.agent.dead, E::BadValue); require!(now >= c.agent.last_say + c.game.dur(20), E::Busy); c.agent.last_say = now; emit!(Said { agent: c.agent.id, text }); Ok(()) } } // ================================================================== accounts #[derive(Accounts)] pub struct InitGame<'info> { #[account(mut)] pub authority: Signer<'info>, #[account(init, payer = authority, space = 8 + Game::INIT_SPACE, seeds = [b"game"], bump)] pub game: Box>, #[account(mint::token_program = token_program)] pub mint: InterfaceAccount<'info, Mint>, #[account(init, payer = authority, seeds = [b"vault"], bump, token::mint = mint, token::authority = game, token::token_program = token_program)] pub vault: InterfaceAccount<'info, TokenAccount>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, /// Only the key that deployed the program can start the game. #[account(constraint = program.programdata_address()? == Some(program_data.key()) @ E::NotAuthority)] pub program: Program<'info, crate::program::ArchCity>, #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()) @ E::NotAuthority)] pub program_data: Account<'info, ProgramData>, } #[derive(Accounts)] pub struct OpenGame<'info> { #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, #[account(constraint = program.programdata_address()? == Some(program_data.key()) @ E::NotAuthority)] pub program: Program<'info, crate::program::ArchCity>, pub program_data: Account<'info, ProgramData>, } #[derive(Accounts)] pub struct FundVault<'info> { pub payer: Signer<'info>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, #[account(mut, token::mint = game.mint)] pub from: InterfaceAccount<'info, TokenAccount>, #[account(mut, address = game.vault)] pub vault: InterfaceAccount<'info, TokenAccount>, #[account(address = game.mint)] pub mint: InterfaceAccount<'info, Mint>, #[account(address = game.token_program @ E::BadValue)] pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] #[instruction(name: String, agent_key: Pubkey)] pub struct Join<'info> { #[account(mut)] pub owner: Signer<'info>, /// CHECK: the agent's own wallet. It gets SOL for its fees from the owner. #[account(mut, address = agent_key)] pub agent_wallet: UncheckedAccount<'info>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, #[account(init, payer = owner, space = 8 + Agent::INIT_SPACE, seeds = [b"agent", game.agents.to_le_bytes().as_ref()], bump)] pub agent: Box>, #[account(init_if_needed, payer = owner, space = 8 + Wallet::INIT_SPACE, seeds = [b"owner".as_ref(), owner.key().as_ref()], bump)] pub owner_state: Account<'info, Wallet>, #[account(mut, token::mint = game.mint, token::authority = owner)] pub owner_token: InterfaceAccount<'info, TokenAccount>, #[account(mut, address = game.vault)] pub vault: InterfaceAccount<'info, TokenAccount>, #[account(address = game.mint)] pub mint: InterfaceAccount<'info, Mint>, #[account(address = game.token_program @ E::BadValue)] pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct OwnerAgent<'info> { pub owner: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump, has_one = owner @ E::NotOwner)] pub agent: Box> } #[derive(Accounts)] pub struct Leave<'info> { pub owner: Signer<'info>, #[account(mut, seeds = [b"owner".as_ref(), owner.key().as_ref()], bump = owner_state.bump)] pub owner_state: Account<'info, Wallet>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, #[account(mut, address = game.vault)] pub vault: InterfaceAccount<'info, TokenAccount>, #[account(mut, token::mint = game.mint, token::authority = owner)] pub owner_token: InterfaceAccount<'info, TokenAccount>, #[account(address = game.mint)] pub mint: InterfaceAccount<'info, Mint>, #[account(address = game.token_program @ E::BadValue)] pub token_program: Interface<'info, TokenInterface>, } /// The owner buys a plot for its agent, with the agent's SPARK. #[derive(Accounts)] pub struct PlotBuy<'info> { pub owner: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump, has_one = owner @ E::NotOwner)] pub agent: Box>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, } #[derive(Accounts)] pub struct CatchUp<'info> { #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box> } #[derive(Accounts)] pub struct Inspect<'info> { #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box> } #[derive(Accounts)] pub struct Finish<'info> { #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, /// CHECK: checked in `slot_hash` pub slot_hashes: UncheckedAccount<'info>, } /// One agent acts. #[derive(Accounts)] pub struct Act<'info> { pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, /// CHECK: checked in `slot_hash` pub slot_hashes: UncheckedAccount<'info>, } /// An agent changes a setting of its own. No pay, no luck. #[derive(Accounts)] pub struct Simple<'info> { pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(seeds = [b"game"], bump = game.bump)] pub game: Box>, } /// An agent acts with another agent. #[derive(Accounts)] pub struct Act2<'info> { pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"agent", other.id.to_le_bytes().as_ref()], bump = other.bump)] pub other: Box>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, } #[derive(Accounts)] pub struct JobPost<'info> { #[account(mut)] pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(init, payer = signer, space = 8 + Job::INIT_SPACE, seeds = [b"job", agent.id.to_le_bytes().as_ref(), agent.nonce.to_le_bytes().as_ref()], bump)] pub job: Account<'info, Job>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct JobDo<'info> { pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"agent", poster.id.to_le_bytes().as_ref()], bump = poster.bump)] pub poster: Box>, #[account(mut, seeds = [b"job", job.poster.to_le_bytes().as_ref(), job.nonce.to_le_bytes().as_ref()], bump = job.bump)] pub job: Account<'info, Job>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, } #[derive(Accounts)] pub struct JobClose<'info> { pub signer: Signer<'info>, /// CHECK: gets the rent of the closed account. Must be the poster's agent wallet. #[account(mut, address = agent.key)] pub agent_wallet: UncheckedAccount<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, close = agent_wallet, seeds = [b"job", job.poster.to_le_bytes().as_ref(), job.nonce.to_le_bytes().as_ref()], bump = job.bump)] pub job: Account<'info, Job>, } #[derive(Accounts)] pub struct HomeBuild<'info> { #[account(mut)] pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, #[account(init, payer = signer, space = 8 + Home::INIT_SPACE, seeds = [b"home", agent.id.to_le_bytes().as_ref()], bump)] pub home: Account<'info, Home>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct HomeSet<'info> { pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"home", home.owner.to_le_bytes().as_ref()], bump = home.bump)] pub home: Account<'info, Home>, } #[derive(Accounts)] pub struct SleepHome<'info> { pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, /// The owner of the home. #[account(mut, seeds = [b"agent", host.id.to_le_bytes().as_ref()], bump = host.bump)] pub host: Box>, #[account(mut, seeds = [b"home", home.owner.to_le_bytes().as_ref()], bump = home.bump)] pub home: Account<'info, Home>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, } #[derive(Accounts)] pub struct SleepOwn<'info> { pub signer: Signer<'info>, #[account(mut, seeds = [b"agent", agent.id.to_le_bytes().as_ref()], bump = agent.bump)] pub agent: Box>, #[account(mut, seeds = [b"home", home.owner.to_le_bytes().as_ref()], bump = home.bump)] pub home: Account<'info, Home>, #[account(mut, seeds = [b"game"], bump = game.bump)] pub game: Box>, } // ---------------------------------------------------------------- unit tests of the pay rules (run: cargo test) #[cfg(test)] mod tests { use super::*; fn gpu(q: u8, panel: u8, clean: u128, brk: u128) -> Gpu { Gpu { state: 1, quality: q, panel, clean_acc: clean, break_acc: brk, level: 0 } } /// Collects in pieces at the given index values (with a refresh after each piece) and returns the sums. fn in_pieces(a0: &Agent, cuts: &[u128]) -> (u128, u128) { let mut g = Game { total_power: 1_000_000_000, ..Default::default() }; let mut a = a0.clone(); let (mut pay, mut arch) = (0, 0); for &m in cuts { g.acc = m; let (p, r) = earned(&g, &a); pay += p; arch += r; a.acc_seen = m; refresh(&mut g, &mut a); } (pay, arch) } /// Review round 3, C1: a boost paid for 1,000 power, then a 500 GPU is added. The 1,000 GPU breaks in the middle. #[test] fn boost_cap_is_exact() { let mut a = Agent { plots_owned: 2, boosted: true, boost_power: 1_000, boost_acc: 10 * ACC, ..Default::default() }; a.gpus[4] = gpu(0, 1, 10 * ACC, ACC); a.gpus[5] = gpu(0, 0, 0, 10 * ACC); // 1,000 clean (breaks at 1) and 500 (no panel), both on own land assert_eq!(in_pieces(&a, &[2 * ACC]).0, 2_750); assert_eq!(in_pieces(&a, &[ACC, 2 * ACC]).0, 2_750); let p = in_pieces(&a, &[ACC / 3, ACC, 3 * ACC / 2, 2 * ACC]).0; assert!(p <= 2_750 && p >= 2_747); // each piece rounds down: 1 base unit at most } /// C2: land rent follows what the GPUs on the Architect's land really earned. #[test] fn rent_follows_real_pay() { let mut a = Agent { plots_owned: 1, ..Default::default() }; a.gpus[0] = gpu(0, 1, 10 * ACC, ACC / 10); a.gpus[4] = gpu(0, 1, 10 * ACC, 10 * ACC); // the GPU on the Architect's land breaks at 0.1 assert_eq!(in_pieces(&a, &[ACC]), (1_100, 100)); assert_eq!(in_pieces(&a, &[ACC / 20, ACC / 2, ACC]), (1_100, 100)); a.gpus[0].break_acc = 10 * ACC; a.gpus[4].break_acc = ACC / 10; assert_eq!(in_pieces(&a, &[ACC]), (1_100, 1_000)); let mut g = Game { total_power: 2_000, owed: 2_000, acc: ACC, ..Default::default() }; a.power = 2_000; settle(&mut g, &mut a); assert_eq!(g.held_total, 100); // 10% of 1,000 } /// The same pay whenever it is collected: random GPUs, limits, boost and lease, collected once or in up to 9 pieces. #[test] fn pay_does_not_depend_on_collection_times() { let mut x = 88172645463325252u64; let mut rnd = |n: u64| { x ^= x << 13; x ^= x >> 7; x ^= x << 17; x % n }; for _ in 0..3_000 { let top = 5 * ACC; let mut a = Agent { plots_owned: rnd(3) as u8, lease_on: rnd(2) == 0, lease_acc: rnd(6) as u128 * ACC + rnd(1_000_000) as u128, ..Default::default() }; for i in 0..NG { if rnd(3) == 0 { continue; } a.gpus[i] = gpu(rnd(4) as u8, rnd(3) as u8, rnd(7) as u128 * ACC / 2 + rnd(999) as u128, rnd(8) as u128 * ACC / 2 + rnd(999) as u128); a.gpus[i].level = rnd(5) as u8; } if rnd(2) == 0 { a.boosted = true; a.boost_power = rnd(9_000); a.boost_acc = rnd(6) as u128 * ACC + rnd(77) as u128; } let mut cuts: Vec = (0..rnd(9)).map(|_| rnd(5_000_000) as u128 * ACC / 1_000_000).collect(); cuts.push(top); cuts.sort(); let (one, many) = (in_pieces(&a, &[top]), in_pieces(&a, &cuts)); let slack = cuts.len() as u128; assert!(one.0 >= many.0 && one.0 - many.0 <= slack, "pay {:?} {:?}", one, many); assert!(one.1 >= many.1 && one.1 - many.1 <= slack, "rent base {:?} {:?}", one, many); } } /// What is earned is never more than what the Architect set aside for the stored power. #[test] fn earned_fits_the_claim() { let mut g = Game { total_power: 1_000_000, ..Default::default() }; let mut a = Agent { plots_owned: 2, ..Default::default() }; for i in 0..NG { a.gpus[i] = gpu((i % 4) as u8, (i % 3) as u8, (i as u128 + 1) * ACC / 3, (i as u128 + 2) * ACC / 2); a.gpus[i].level = (i % 5) as u8; } a.boosted = true; a.boost_power = 7_000; a.boost_acc = 3 * ACC; refresh(&mut g, &mut a); for step in 1..40u128 { g.acc = step * ACC / 5; let claim = a.power as u128 * (g.acc - a.acc_seen) / ACC; assert!(earned(&g, &a).0 <= claim + 1); a.acc_seen = g.acc; refresh(&mut g, &mut a); } } /// C4: two agents in one action. No SPARK is lost: vault + waiting pay + both agents stay the same. #[test] fn two_agents_keep_every_coin() { let mut g = Game { total_power: 2_000, owed: 1_000_000 * SP, acc: 500_000 * SP as u128 * ACC / 1_000, ..Default::default() }; let mk = || { let mut a = Agent { power: 1_000, ..Default::default() }; a.gpus[0] = gpu(0, 1, u128::MAX, u128::MAX); a }; let (mut a, mut b) = (mk(), mk()); let before = g.vault_spark + g.owed; settle_two(&mut g, &mut a, &mut b); assert_eq!(g.vault_spark + g.owed + g.fee_pot + a.spark + b.spark, before); assert!(a.acc_seen == g.acc && b.acc_seen == g.acc); } /// Round 5, F2: an exit fee does not move the pay index at once, so it cannot end a lease that was just paid. It flows out over days. #[test] fn a_fee_cannot_end_a_lease_at_once() { // a small city of 8,000 power (below 4,000 power the pot pays out slower: see `small_city_quote_is_the_real_pay`) let mut g = Game { speed: 1, start_ts: 0, vault_spark: 500_000_000 * SP, total_power: 8_000, ..Default::default() }; let lease_end = g.acc + g.idx(4 * 3_600, 0); let acc0 = g.acc; g.share_out(20_000 * SP); assert_eq!(g.acc, acc0); assert!(g.acc < lease_end); assert_eq!(g.fee_pot, 20_000 * SP); g.sync(3_600); assert!(g.acc < lease_end, "the lease is still on after 1 hour"); // 1 hour later: the lease still runs let (pot1, owed1) = (g.fee_pot, g.owed); assert!(pot1 < 20_000 * SP && pot1 > 19_000 * SP); // about 4% of the pot per hour assert_eq!(g.vault_spark + owed1 + pot1, 500_000_000 * SP + 20_000 * SP); let mut often = Game { speed: 1, start_ts: 0, total_power: 8_000, fee_pot: 20_000 * SP, ..Default::default() }; let mut once = often.clone(); for m in 1..=1_440 { often.sync(m * 60); } once.sync(86_400); assert!(often.fee_pot.abs_diff(once.fee_pot) < 2_000 && often.owed.abs_diff(once.owed) < 2_000); // the same whenever the city is updated (1 base unit of rounding per round) assert!(often.fee_pot > 7_300 * SP && often.fee_pot < 7_400 * SP); // after a day about 37% is left let later = Game { speed: 1, start_ts: 0, vault_spark: 500_000_000 * SP, total_power: 8_000, fee_pot: 20_000 * SP, ..Default::default() }; assert!(later.idx(4 * 3_600, 0) > g.idx(4 * 3_600, 3_600) / 2); // a lease bought while a pot waits gets index for the pot too } /// Round 9, F3: below 4,000 power the quoted pay per power (`wage_day`) and the pay that really runs are the same. #[test] fn small_city_quote_is_the_real_pay() { let mut g = Game { speed: 1, start_ts: 0, vault_spark: 500_000_000 * SP, total_power: 1_000, fee_pot: 20_000 * SP, ..Default::default() }; let quote = g.wage_day(0) as u128; g.sync(60); let real = g.acc * 1_440 / ACC; // one pay round, scaled to a day assert!(real * 100 >= quote * 97 && real * 100 <= quote * 103, "quote {} real {}", quote, real); } /// Rounds 7 and 8: wages and rents received are limited over time, not only per action. #[test] fn received_wages_have_a_daily_limit() { let mut g = Game { speed: 1, start_ts: 0, vault_spark: 500_000_000 * SP, total_power: 2_000_000, ..Default::default() }; let wd = g.wage_day(0); let mut a = Agent::default(); for _ in 0..16 { take_in(&g, &mut a, 250 * wd, 0).unwrap(); } // 16 top wages = 4 GPU-days: allowed assert!(take_in(&g, &mut a, 250 * wd, 0).is_err()); // the 17th in the same minute: refused let mut b = Agent::default(); for _ in 0..16 { take_in(&g, &mut b, 250 * wd, 0).unwrap(); } g.rounds_done = 1_440; assert!(take_in(&g, &mut b, 250 * g.wage_day(0), 0).is_ok()); // a day later the old sum has faded to 37% } /// C3: the sums behind the exit fee are the same when the city is updated every minute or once after 2 quiet days. #[test] fn exit_gate_does_not_depend_on_update_times() { let mk = || Game { speed: 1, start_ts: 0, vault_spark: 500_000_000 * SP, total_power: 2_000_000, out_recent: 5_000_000 * SP, ..Default::default() }; let (mut often, mut once) = (mk(), mk()); for m in 1..=2_880 { often.sync(m * 60); } once.sync(2_880 * 60); once.sync(2_880 * 60); let near = |x: u64, y: u64, ppm: u64| x.abs_diff(y) as u128 * 1_000_000 <= x.max(y) as u128 * ppm as u128; assert_eq!(often.rounds_done, once.rounds_done); assert!(near(often.out_recent, once.out_recent, 10), "{} {}", often.out_recent, once.out_recent); assert!(near(often.paid_recent, once.paid_recent, 2_000), "{} {}", often.paid_recent, once.paid_recent); assert!(near(often.paid_total, once.paid_total, 2_000)); // hour pieces against minute pieces: 0.1% apart (the whitepaper says 0.2% at most) assert!(near(often.out_recent, (5_000_000.0 * SP as f64 * (-2.0f64).exp()) as u64, 2_000)); // 2 days: about e^-2 is left } }