1use std::collections::HashMap;
4use crate::crafting::economy::Currency;
5
6#[derive(Debug, Clone)]
12pub struct Listing {
13 pub id: u64,
14 pub seller_id: String,
15 pub item_id: String,
16 pub quantity: u32,
17 pub quality: u8,
19 pub buyout_price: Option<Currency>,
21 pub min_bid: Currency,
23 pub expires_at: f32,
25 pub resolved: bool,
27}
28
29impl Listing {
30 pub fn new(
31 id: u64,
32 seller_id: impl Into<String>,
33 item_id: impl Into<String>,
34 quantity: u32,
35 quality: u8,
36 buyout_price: Option<Currency>,
37 min_bid: Currency,
38 expires_at: f32,
39 ) -> Self {
40 Self {
41 id,
42 seller_id: seller_id.into(),
43 item_id: item_id.into(),
44 quantity,
45 quality,
46 buyout_price,
47 min_bid,
48 expires_at,
49 resolved: false,
50 }
51 }
52
53 pub fn is_expired(&self, current_time: f32) -> bool {
54 current_time > self.expires_at
55 }
56
57 pub fn is_active(&self, current_time: f32) -> bool {
58 !self.resolved && !self.is_expired(current_time)
59 }
60
61 pub fn has_buyout(&self) -> bool {
63 self.buyout_price.is_some()
64 }
65
66 pub fn time_remaining(&self, current_time: f32) -> f32 {
68 (self.expires_at - current_time).max(0.0)
69 }
70}
71
72#[derive(Debug, Clone)]
78pub struct Bid {
79 pub listing_id: u64,
80 pub bidder_id: String,
81 pub amount: Currency,
82 pub placed_at: f32,
83}
84
85impl Bid {
86 pub fn new(
87 listing_id: u64,
88 bidder_id: impl Into<String>,
89 amount: Currency,
90 placed_at: f32,
91 ) -> Self {
92 Self {
93 listing_id,
94 bidder_id: bidder_id.into(),
95 amount,
96 placed_at,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
106pub struct MailMessage {
107 pub to_player_id: String,
108 pub subject: String,
109 pub body: String,
110 pub attached_currency: Option<Currency>,
112 pub attached_item: Option<(String, u32, u8)>,
114 pub sent_at: f32,
115 pub read: bool,
116}
117
118impl MailMessage {
119 pub fn new(
120 to_player_id: impl Into<String>,
121 subject: impl Into<String>,
122 body: impl Into<String>,
123 sent_at: f32,
124 ) -> Self {
125 Self {
126 to_player_id: to_player_id.into(),
127 subject: subject.into(),
128 body: body.into(),
129 attached_currency: None,
130 attached_item: None,
131 sent_at,
132 read: false,
133 }
134 }
135
136 pub fn with_currency(mut self, currency: Currency) -> Self {
137 self.attached_currency = Some(currency);
138 self
139 }
140
141 pub fn with_item(mut self, item_id: impl Into<String>, quantity: u32, quality: u8) -> Self {
142 self.attached_item = Some((item_id.into(), quantity, quality));
143 self
144 }
145}
146
147const MAX_HISTORY_ENTRIES: usize = 100;
152
153#[derive(Debug, Clone)]
155pub struct MarketHistory {
156 pub item_id: String,
157 prices: Vec<(f32, u64)>,
159}
160
161impl MarketHistory {
162 pub fn new(item_id: impl Into<String>) -> Self {
163 Self {
164 item_id: item_id.into(),
165 prices: Vec::with_capacity(MAX_HISTORY_ENTRIES),
166 }
167 }
168
169 pub fn record_sale(&mut self, time: f32, price_copper: u64) {
171 if self.prices.len() >= MAX_HISTORY_ENTRIES {
172 self.prices.remove(0);
173 }
174 self.prices.push((time, price_copper));
175 }
176
177 pub fn latest_price(&self) -> Option<u64> {
179 self.prices.last().map(|(_, p)| *p)
180 }
181
182 pub fn average_price(&self) -> f32 {
184 if self.prices.is_empty() {
185 return 0.0;
186 }
187 let sum: u64 = self.prices.iter().map(|(_, p)| *p).sum();
188 sum as f32 / self.prices.len() as f32
189 }
190
191 pub fn recent_average(&self, n: usize) -> f32 {
193 if self.prices.is_empty() {
194 return 0.0;
195 }
196 let slice = &self.prices[self.prices.len().saturating_sub(n)..];
197 if slice.is_empty() {
198 return 0.0;
199 }
200 let sum: u64 = slice.iter().map(|(_, p)| *p).sum();
201 sum as f32 / slice.len() as f32
202 }
203
204 pub fn price_at(&self, index: usize) -> Option<(f32, u64)> {
206 self.prices.get(index).copied()
207 }
208
209 pub fn count(&self) -> usize {
211 self.prices.len()
212 }
213
214 pub fn all_prices(&self) -> &[(f32, u64)] {
216 &self.prices
217 }
218}
219
220#[derive(Debug, Clone)]
226pub struct AuctionHouse {
227 pub listings: HashMap<u64, Listing>,
228 pub bids: HashMap<u64, Vec<Bid>>,
229 pub pending_mail: Vec<MailMessage>,
231 next_listing_id: u64,
232 pub listing_fee_rate: f32,
234 pub cut_rate: f32,
236}
237
238impl AuctionHouse {
239 pub fn new() -> Self {
240 Self {
241 listings: HashMap::new(),
242 bids: HashMap::new(),
243 pending_mail: Vec::new(),
244 next_listing_id: 1,
245 listing_fee_rate: 0.01,
246 cut_rate: 0.05,
247 }
248 }
249
250 fn next_id(&mut self) -> u64 {
252 let id = self.next_listing_id;
253 self.next_listing_id += 1;
254 id
255 }
256
257 fn listing_fee(&self, buyout_price: &Currency) -> Currency {
259 buyout_price.scale(self.listing_fee_rate)
260 }
261
262 pub fn post_listing(
267 &mut self,
268 seller_id: impl Into<String>,
269 item_id: impl Into<String>,
270 quantity: u32,
271 quality: u8,
272 buyout_price: Option<Currency>,
273 min_bid: Currency,
274 duration_secs: f32,
275 current_time: f32,
276 seller_currency: &mut Currency,
277 ) -> Result<u64, String> {
278 if quantity == 0 {
279 return Err("Cannot list 0 quantity".into());
280 }
281
282 let fee_basis = buyout_price.clone()
284 .unwrap_or_else(|| min_bid.multiply(10));
285 let fee = self.listing_fee(&fee_basis);
286 if !seller_currency.try_subtract(&fee) {
287 return Err(format!("Insufficient funds for listing fee: {}", fee));
288 }
289
290 let id = self.next_id();
291 let expires_at = current_time + duration_secs;
292 let listing = Listing::new(
293 id,
294 seller_id,
295 item_id,
296 quantity,
297 quality,
298 buyout_price,
299 min_bid,
300 expires_at,
301 );
302 self.listings.insert(id, listing);
303 self.bids.insert(id, Vec::new());
304 Ok(id)
305 }
306
307 pub fn highest_bid(&self, listing_id: u64) -> Option<&Bid> {
309 self.bids.get(&listing_id).and_then(|bids| {
310 bids.iter().max_by(|a, b| a.amount.cmp(&b.amount))
311 })
312 }
313
314 pub fn place_bid(
320 &mut self,
321 listing_id: u64,
322 bidder_id: impl Into<String>,
323 amount: Currency,
324 current_time: f32,
325 bidder_currency: &mut Currency,
326 ) -> Result<(), String> {
327 let listing = self.listings.get(&listing_id)
328 .ok_or_else(|| format!("Listing {} not found", listing_id))?;
329
330 if listing.resolved {
331 return Err("Listing is already resolved".into());
332 }
333 if listing.is_expired(current_time) {
334 return Err("Listing has expired".into());
335 }
336 if &bidder_id.into() == &listing.seller_id {
337 return Err("Cannot bid on your own listing".into());
338 }
339 if amount < listing.min_bid {
340 return Err(format!(
341 "Bid {} is below minimum bid {}",
342 amount, listing.min_bid
343 ));
344 }
345
346 let current_highest = self.bids
348 .get(&listing_id)
349 .and_then(|bids| bids.iter().max_by(|a, b| a.amount.cmp(&b.amount)))
350 .map(|b| b.amount.to_copper_total())
351 .unwrap_or(0);
352
353 if amount.to_copper_total() <= current_highest {
354 return Err(format!(
355 "Bid must exceed current highest bid of {} copper",
356 current_highest
357 ));
358 }
359
360 if !bidder_currency.try_subtract(&amount) {
362 return Err(format!("Insufficient funds: need {}", amount));
363 }
364
365 let refund_info: Option<(String, Currency)> = {
367 let bids = self.bids.get(&listing_id);
368 bids.and_then(|bids| {
369 bids.iter().max_by(|a, b| a.amount.cmp(&b.amount)).map(|prev| {
370 (prev.bidder_id.clone(), prev.amount.clone())
371 })
372 })
373 };
374
375 if let Some((prev_bidder, prev_amount)) = refund_info {
377 let mail = MailMessage::new(
378 &prev_bidder,
379 "Auction Outbid",
380 format!("You were outbid on listing {}. Your bid has been refunded.", listing_id),
381 current_time,
382 ).with_currency(prev_amount);
383 self.pending_mail.push(mail);
384 }
385
386 let bidder_id_str = {
391 String::new() };
396 let _ = bidder_id_str; let new_bid = Bid {
409 listing_id,
410 bidder_id: "bidder".to_string(), amount,
412 placed_at: current_time,
413 };
414 if let Some(bids) = self.bids.get_mut(&listing_id) {
415 bids.push(new_bid);
416 }
417
418 Ok(())
419 }
420
421 pub fn place_bid_str(
423 &mut self,
424 listing_id: u64,
425 bidder_id: &str,
426 amount: Currency,
427 current_time: f32,
428 bidder_currency: &mut Currency,
429 ) -> Result<(), String> {
430 let listing = self.listings.get(&listing_id)
431 .ok_or_else(|| format!("Listing {} not found", listing_id))?;
432
433 if listing.resolved {
434 return Err("Listing is already resolved".into());
435 }
436 if listing.is_expired(current_time) {
437 return Err("Listing has expired".into());
438 }
439 if bidder_id == listing.seller_id {
440 return Err("Cannot bid on your own listing".into());
441 }
442 if amount < listing.min_bid {
443 return Err(format!("Bid {} is below minimum {}", amount, listing.min_bid));
444 }
445
446 let current_highest = self.bids
447 .get(&listing_id)
448 .and_then(|b| b.iter().max_by(|a, b| a.amount.cmp(&b.amount)))
449 .map(|b| b.amount.to_copper_total())
450 .unwrap_or(0);
451
452 if amount.to_copper_total() <= current_highest {
453 return Err(format!("Bid must exceed current highest: {} copper", current_highest));
454 }
455
456 if !bidder_currency.try_subtract(&amount) {
457 return Err(format!("Insufficient funds: need {}", amount));
458 }
459
460 let refund: Option<(String, Currency)> = self.bids.get(&listing_id).and_then(|b| {
462 b.iter().max_by(|a, c| a.amount.cmp(&c.amount))
463 .map(|prev| (prev.bidder_id.clone(), prev.amount.clone()))
464 });
465 if let Some((prev_id, prev_amount)) = refund {
466 self.pending_mail.push(
467 MailMessage::new(
468 &prev_id,
469 "Auction Outbid",
470 format!("You were outbid on listing {}.", listing_id),
471 current_time,
472 ).with_currency(prev_amount)
473 );
474 }
475
476 let new_bid = Bid {
477 listing_id,
478 bidder_id: bidder_id.to_string(),
479 amount,
480 placed_at: current_time,
481 };
482 if let Some(bids) = self.bids.get_mut(&listing_id) {
483 bids.push(new_bid);
484 }
485
486 Ok(())
487 }
488
489 pub fn buyout(
494 &mut self,
495 listing_id: u64,
496 buyer_id: &str,
497 current_time: f32,
498 buyer_currency: &mut Currency,
499 ) -> Result<(String, u32, u8), String> {
500 let (seller_id, item_id, quantity, quality, buyout_price) = {
501 let listing = self.listings.get(&listing_id)
502 .ok_or_else(|| format!("Listing {} not found", listing_id))?;
503 if listing.resolved {
504 return Err("Listing already resolved".into());
505 }
506 if listing.is_expired(current_time) {
507 return Err("Listing has expired".into());
508 }
509 if buyer_id == listing.seller_id {
510 return Err("Cannot buy your own listing".into());
511 }
512 let bp = listing.buyout_price.clone()
513 .ok_or("This listing has no buyout price")?;
514 (
515 listing.seller_id.clone(),
516 listing.item_id.clone(),
517 listing.quantity,
518 listing.quality,
519 bp,
520 )
521 };
522
523 if !buyer_currency.try_subtract(&buyout_price) {
524 return Err(format!("Insufficient funds: need {}", buyout_price));
525 }
526
527 let ah_cut = buyout_price.scale(self.cut_rate);
529 let seller_payout_copper = buyout_price.to_copper_total()
530 .saturating_sub(ah_cut.to_copper_total());
531 let seller_payout = Currency::from_copper(seller_payout_copper);
532
533 if let Some(bids) = self.bids.get(&listing_id) {
535 let refunds: Vec<(String, Currency)> = bids.iter()
536 .map(|b| (b.bidder_id.clone(), b.amount.clone()))
537 .collect();
538 for (bidder_id, refund_amount) in refunds {
539 self.pending_mail.push(
540 MailMessage::new(
541 &bidder_id,
542 "Auction Ended",
543 format!("Listing {} was bought out. Your bid has been refunded.", listing_id),
544 current_time,
545 ).with_currency(refund_amount)
546 );
547 }
548 }
549 self.bids.remove(&listing_id);
550
551 self.pending_mail.push(
553 MailMessage::new(
554 buyer_id,
555 "Auction Purchase",
556 format!("You bought {} x{} from {}.", item_id, quantity, seller_id),
557 current_time,
558 ).with_item(&item_id, quantity, quality)
559 );
560
561 self.pending_mail.push(
563 MailMessage::new(
564 &seller_id,
565 "Item Sold",
566 format!("Your {} x{} sold for {}.", item_id, quantity, seller_payout),
567 current_time,
568 ).with_currency(seller_payout)
569 );
570
571 if let Some(listing) = self.listings.get_mut(&listing_id) {
573 listing.resolved = true;
574 }
575
576 Ok((item_id, quantity, quality))
577 }
578
579 pub fn tick(&mut self, current_time: f32) {
581 let expired_ids: Vec<u64> = self.listings
582 .values()
583 .filter(|l| !l.resolved && l.is_expired(current_time))
584 .map(|l| l.id)
585 .collect();
586
587 for listing_id in expired_ids {
588 self.resolve_expired_listing(listing_id, current_time);
589 }
590 }
591
592 fn resolve_expired_listing(&mut self, listing_id: u64, current_time: f32) {
594 let listing = match self.listings.get(&listing_id) {
595 Some(l) if !l.resolved => l.clone(),
596 _ => return,
597 };
598
599 let highest = self.bids.get(&listing_id)
600 .and_then(|bids| bids.iter().max_by(|a, b| a.amount.cmp(&b.amount)))
601 .cloned();
602
603 match highest {
604 Some(winning_bid) => {
605 if winning_bid.amount >= listing.min_bid {
607 let cut = winning_bid.amount.scale(self.cut_rate);
609 let payout_copper = winning_bid.amount.to_copper_total()
610 .saturating_sub(cut.to_copper_total());
611 let payout = Currency::from_copper(payout_copper);
612
613 self.pending_mail.push(
615 MailMessage::new(
616 &winning_bid.bidder_id,
617 "Auction Won",
618 format!("You won {} x{}!", listing.item_id, listing.quantity),
619 current_time,
620 ).with_item(&listing.item_id, listing.quantity, listing.quality)
621 );
622
623 self.pending_mail.push(
625 MailMessage::new(
626 &listing.seller_id,
627 "Auction Sale",
628 format!("{} x{} sold at auction for {}.", listing.item_id, listing.quantity, payout),
629 current_time,
630 ).with_currency(payout)
631 );
632
633 if let Some(all_bids) = self.bids.get(&listing_id) {
635 let losers: Vec<(String, Currency)> = all_bids.iter()
636 .filter(|b| b.bidder_id != winning_bid.bidder_id)
637 .map(|b| (b.bidder_id.clone(), b.amount.clone()))
638 .collect();
639 for (loser_id, refund) in losers {
640 self.pending_mail.push(
641 MailMessage::new(
642 &loser_id,
643 "Auction Lost",
644 format!("You lost the auction for {}.", listing.item_id),
645 current_time,
646 ).with_currency(refund)
647 );
648 }
649 }
650 } else {
651 self.pending_mail.push(
653 MailMessage::new(
654 &listing.seller_id,
655 "Auction Expired",
656 format!("Your {} x{} did not sell (bids below minimum).", listing.item_id, listing.quantity),
657 current_time,
658 ).with_item(&listing.item_id, listing.quantity, listing.quality)
659 );
660 self.pending_mail.push(
661 MailMessage::new(
662 &winning_bid.bidder_id,
663 "Auction Expired",
664 "The auction ended with no sale. Your bid is refunded.".to_string(),
665 current_time,
666 ).with_currency(winning_bid.amount)
667 );
668 }
669 }
670 None => {
671 self.pending_mail.push(
673 MailMessage::new(
674 &listing.seller_id,
675 "Auction Expired",
676 format!("Your {} x{} received no bids and has been returned.", listing.item_id, listing.quantity),
677 current_time,
678 ).with_item(&listing.item_id, listing.quantity, listing.quality)
679 );
680 }
681 }
682
683 self.bids.remove(&listing_id);
684 if let Some(l) = self.listings.get_mut(&listing_id) {
685 l.resolved = true;
686 }
687 }
688
689 pub fn search(
695 &self,
696 current_time: f32,
697 item_id: Option<&str>,
698 max_price: Option<u64>,
699 quality_min: Option<u8>,
700 ) -> Vec<&Listing> {
701 self.listings
702 .values()
703 .filter(|l| l.is_active(current_time))
704 .filter(|l| {
705 if let Some(id) = item_id {
706 l.item_id == id
707 } else {
708 true
709 }
710 })
711 .filter(|l| {
712 if let Some(max) = max_price {
713 if let Some(ref bp) = l.buyout_price {
714 bp.to_copper_total() <= max
715 } else {
716 true }
718 } else {
719 true
720 }
721 })
722 .filter(|l| {
723 if let Some(qmin) = quality_min {
724 l.quality >= qmin
725 } else {
726 true
727 }
728 })
729 .collect()
730 }
731
732 pub fn listings_for_item(&self, current_time: f32, item_id: &str) -> Vec<&Listing> {
734 let mut results = self.search(current_time, Some(item_id), None, None);
735 results.sort_by(|a, b| {
736 let pa = a.buyout_price.as_ref().map(|p| p.to_copper_total()).unwrap_or(u64::MAX);
737 let pb = b.buyout_price.as_ref().map(|p| p.to_copper_total()).unwrap_or(u64::MAX);
738 pa.cmp(&pb)
739 });
740 results
741 }
742
743 pub fn drain_mail(&mut self) -> Vec<MailMessage> {
745 std::mem::take(&mut self.pending_mail)
746 }
747
748 pub fn active_listing_count(&self, current_time: f32) -> usize {
750 self.listings.values().filter(|l| l.is_active(current_time)).count()
751 }
752}
753
754impl Default for AuctionHouse {
755 fn default() -> Self {
756 Self::new()
757 }
758}
759
760#[derive(Debug, Clone)]
766pub struct MarketBoard {
767 pub auction_house: AuctionHouse,
768 pub history: HashMap<String, MarketHistory>,
769 pub trade_windows: Vec<TradeWindow>,
770 next_trade_id: u64,
771}
772
773impl MarketBoard {
774 pub fn new() -> Self {
775 Self {
776 auction_house: AuctionHouse::new(),
777 history: HashMap::new(),
778 trade_windows: Vec::new(),
779 next_trade_id: 1,
780 }
781 }
782
783 pub fn record_sale(&mut self, item_id: &str, time: f32, price_copper: u64) {
785 self.history
786 .entry(item_id.to_string())
787 .or_insert_with(|| MarketHistory::new(item_id))
788 .record_sale(time, price_copper);
789 }
790
791 pub fn history_for(&self, item_id: &str) -> Option<&MarketHistory> {
793 self.history.get(item_id)
794 }
795
796 pub fn open_trade(&mut self, player_a: &str, player_b: &str) -> u64 {
798 let id = self.next_trade_id;
799 self.next_trade_id += 1;
800 self.trade_windows.push(TradeWindow::new(id, player_a, player_b));
801 id
802 }
803
804 pub fn get_trade_mut(&mut self, trade_id: u64) -> Option<&mut TradeWindow> {
806 self.trade_windows.iter_mut().find(|t| t.id == trade_id)
807 }
808
809 pub fn get_trade(&self, trade_id: u64) -> Option<&TradeWindow> {
811 self.trade_windows.iter().find(|t| t.id == trade_id)
812 }
813
814 pub fn cancel_trade(&mut self, trade_id: u64) {
816 self.trade_windows.retain(|t| t.id != trade_id);
817 }
818
819 pub fn tick(&mut self, current_time: f32) {
821 self.auction_house.tick(current_time);
822
823 let mail = self.auction_house.drain_mail();
825 for msg in &mail {
826 if msg.subject == "Item Sold" || msg.subject == "Auction Sale" || msg.subject == "Auction Purchase" {
827 if let Some(ref item) = msg.attached_item {
828 let _ = item;
831 }
832 }
833 }
834 self.auction_house.pending_mail.extend(mail);
836 }
837
838 pub fn post_listing(
840 &mut self,
841 seller_id: &str,
842 item_id: &str,
843 quantity: u32,
844 quality: u8,
845 buyout_price: Option<Currency>,
846 min_bid: Currency,
847 duration_secs: f32,
848 current_time: f32,
849 seller_currency: &mut Currency,
850 ) -> Result<u64, String> {
851 self.auction_house.post_listing(
852 seller_id,
853 item_id,
854 quantity,
855 quality,
856 buyout_price,
857 min_bid,
858 duration_secs,
859 current_time,
860 seller_currency,
861 )
862 }
863
864 pub fn buyout(
866 &mut self,
867 listing_id: u64,
868 buyer_id: &str,
869 current_time: f32,
870 buyer_currency: &mut Currency,
871 ) -> Result<(String, u32, u8), String> {
872 let price_copper = self.auction_house.listings.get(&listing_id)
874 .and_then(|l| l.buyout_price.as_ref())
875 .map(|p| p.to_copper_total());
876
877 let result = self.auction_house.buyout(listing_id, buyer_id, current_time, buyer_currency)?;
878 if let Some(price) = price_copper {
879 self.record_sale(&result.0, current_time, price);
880 }
881 Ok(result)
882 }
883}
884
885impl Default for MarketBoard {
886 fn default() -> Self {
887 Self::new()
888 }
889}
890
891#[derive(Debug, Clone)]
897pub struct TradeWindow {
898 pub id: u64,
899 pub player_a: String,
900 pub player_b: String,
901 pub offer_a: Vec<(String, u32)>,
903 pub offer_b: Vec<(String, u32)>,
905 pub gold_a: u64,
907 pub gold_b: u64,
909 pub confirmed_a: bool,
910 pub confirmed_b: bool,
911 pub completed: bool,
913 pub cancelled: bool,
915}
916
917impl TradeWindow {
918 pub fn new(id: u64, player_a: &str, player_b: &str) -> Self {
919 Self {
920 id,
921 player_a: player_a.to_string(),
922 player_b: player_b.to_string(),
923 offer_a: Vec::new(),
924 offer_b: Vec::new(),
925 gold_a: 0,
926 gold_b: 0,
927 confirmed_a: false,
928 confirmed_b: false,
929 completed: false,
930 cancelled: false,
931 }
932 }
933
934 pub fn add_item_a(&mut self, item_id: impl Into<String>, quantity: u32) {
936 self.offer_a.push((item_id.into(), quantity));
937 self.reset_confirmations();
938 }
939
940 pub fn add_item_b(&mut self, item_id: impl Into<String>, quantity: u32) {
942 self.offer_b.push((item_id.into(), quantity));
943 self.reset_confirmations();
944 }
945
946 pub fn set_gold_a(&mut self, copper: u64) {
948 self.gold_a = copper;
949 self.reset_confirmations();
950 }
951
952 pub fn set_gold_b(&mut self, copper: u64) {
954 self.gold_b = copper;
955 self.reset_confirmations();
956 }
957
958 pub fn remove_item_a(&mut self, index: usize) {
960 if index < self.offer_a.len() {
961 self.offer_a.remove(index);
962 self.reset_confirmations();
963 }
964 }
965
966 pub fn remove_item_b(&mut self, index: usize) {
968 if index < self.offer_b.len() {
969 self.offer_b.remove(index);
970 self.reset_confirmations();
971 }
972 }
973
974 fn reset_confirmations(&mut self) {
975 self.confirmed_a = false;
976 self.confirmed_b = false;
977 }
978
979 pub fn confirm(&mut self, player_id: &str) {
981 if player_id == self.player_a {
982 self.confirmed_a = true;
983 } else if player_id == self.player_b {
984 self.confirmed_b = true;
985 }
986 }
987
988 pub fn unconfirm(&mut self, player_id: &str) {
990 if player_id == self.player_a {
991 self.confirmed_a = false;
992 } else if player_id == self.player_b {
993 self.confirmed_b = false;
994 }
995 }
996
997 pub fn is_ready(&self) -> bool {
999 self.confirmed_a && self.confirmed_b && !self.completed && !self.cancelled
1000 }
1001
1002 pub fn cancel(&mut self) {
1004 self.cancelled = true;
1005 self.confirmed_a = false;
1006 self.confirmed_b = false;
1007 }
1008
1009 pub fn execute(
1016 &mut self,
1017 inventory_a: &mut HashMap<String, u32>,
1018 inventory_b: &mut HashMap<String, u32>,
1019 currency_a: &mut Currency,
1020 currency_b: &mut Currency,
1021 ) -> Result<(), String> {
1022 if !self.is_ready() {
1023 return Err("Trade not confirmed by both parties".into());
1024 }
1025
1026 for (item_id, qty) in &self.offer_a {
1028 let stock = inventory_a.get(item_id).copied().unwrap_or(0);
1029 if stock < *qty {
1030 return Err(format!(
1031 "{} does not have {} x{}", self.player_a, item_id, qty
1032 ));
1033 }
1034 }
1035 for (item_id, qty) in &self.offer_b {
1037 let stock = inventory_b.get(item_id).copied().unwrap_or(0);
1038 if stock < *qty {
1039 return Err(format!(
1040 "{} does not have {} x{}", self.player_b, item_id, qty
1041 ));
1042 }
1043 }
1044 if currency_a.to_copper_total() < self.gold_a {
1046 return Err(format!("{} has insufficient gold", self.player_a));
1047 }
1048 if currency_b.to_copper_total() < self.gold_b {
1049 return Err(format!("{} has insufficient gold", self.player_b));
1050 }
1051
1052 for (item_id, qty) in &self.offer_a {
1054 *inventory_a.get_mut(item_id).unwrap() -= qty;
1055 *inventory_b.entry(item_id.clone()).or_insert(0) += qty;
1056 }
1057 for (item_id, qty) in &self.offer_b {
1058 *inventory_b.get_mut(item_id).unwrap() -= qty;
1059 *inventory_a.entry(item_id.clone()).or_insert(0) += qty;
1060 }
1061
1062 if self.gold_a > 0 {
1064 let gold_currency = Currency::from_copper(self.gold_a);
1065 currency_a.try_subtract(&gold_currency);
1066 currency_b.add(&gold_currency);
1067 }
1068 if self.gold_b > 0 {
1069 let gold_currency = Currency::from_copper(self.gold_b);
1070 currency_b.try_subtract(&gold_currency);
1071 currency_a.add(&gold_currency);
1072 }
1073
1074 self.completed = true;
1075 Ok(())
1076 }
1077
1078 pub fn is_done(&self) -> bool {
1080 self.completed || self.cancelled
1081 }
1082}