first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,401 @@
"""
Marketplace and economy data models.
This module defines the marketplace-related dataclasses including
MarketplaceListing, Bid, Transaction, and ShopItem for the player economy.
"""
from dataclasses import dataclass, field, asdict
from typing import Dict, Any, List, Optional
from datetime import datetime
from app.models.items import Item
from app.models.enums import ListingType, ListingStatus
@dataclass
class Bid:
"""
Represents a bid on an auction listing.
Attributes:
bidder_id: User ID of the bidder
bidder_name: Character name of the bidder
amount: Bid amount in gold
timestamp: ISO timestamp of when bid was placed
"""
bidder_id: str
bidder_name: str
amount: int
timestamp: str = ""
def __post_init__(self):
"""Initialize timestamp if not provided."""
if not self.timestamp:
self.timestamp = datetime.utcnow().isoformat()
def to_dict(self) -> Dict[str, Any]:
"""Serialize bid to dictionary."""
return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Bid':
"""Deserialize bid from dictionary."""
return cls(
bidder_id=data["bidder_id"],
bidder_name=data["bidder_name"],
amount=data["amount"],
timestamp=data.get("timestamp", ""),
)
@dataclass
class MarketplaceListing:
"""
Represents an item listing on the player marketplace.
Supports both fixed-price and auction-style listings.
Attributes:
listing_id: Unique identifier
seller_id: User ID of the seller
character_id: Character ID of the seller
item_data: Full item details being sold
listing_type: "auction" or "fixed_price"
price: For fixed_price listings
starting_bid: Minimum bid for auction listings
current_bid: Current highest bid for auction listings
buyout_price: Optional instant-buy price for auctions
bids: Bid history for auction listings
auction_end: ISO timestamp when auction ends
status: Listing status (active, sold, expired, removed)
created_at: ISO timestamp of listing creation
"""
listing_id: str
seller_id: str
character_id: str
item_data: Item
listing_type: ListingType
status: ListingStatus = ListingStatus.ACTIVE
created_at: str = ""
# Fixed price fields
price: int = 0
# Auction fields
starting_bid: int = 0
current_bid: int = 0
buyout_price: int = 0
bids: List[Bid] = field(default_factory=list)
auction_end: str = ""
def __post_init__(self):
"""Initialize timestamps if not provided."""
if not self.created_at:
self.created_at = datetime.utcnow().isoformat()
def is_auction(self) -> bool:
"""Check if this is an auction listing."""
return self.listing_type == ListingType.AUCTION
def is_fixed_price(self) -> bool:
"""Check if this is a fixed-price listing."""
return self.listing_type == ListingType.FIXED_PRICE
def is_active(self) -> bool:
"""Check if listing is active."""
return self.status == ListingStatus.ACTIVE
def has_ended(self) -> bool:
"""Check if auction has ended (for auction listings)."""
if not self.is_auction() or not self.auction_end:
return False
end_time = datetime.fromisoformat(self.auction_end)
return datetime.utcnow() >= end_time
def can_bid(self, bid_amount: int) -> bool:
"""
Check if a bid amount is valid.
Args:
bid_amount: Proposed bid amount
Returns:
True if bid is valid, False otherwise
"""
if not self.is_auction() or not self.is_active():
return False
if self.has_ended():
return False
# First bid must meet starting bid
if not self.bids and bid_amount < self.starting_bid:
return False
# Subsequent bids must exceed current bid
if self.bids and bid_amount <= self.current_bid:
return False
return True
def place_bid(self, bidder_id: str, bidder_name: str, amount: int) -> bool:
"""
Place a bid on this auction.
Args:
bidder_id: User ID of bidder
bidder_name: Character name of bidder
amount: Bid amount
Returns:
True if bid was accepted, False otherwise
"""
if not self.can_bid(amount):
return False
bid = Bid(
bidder_id=bidder_id,
bidder_name=bidder_name,
amount=amount,
)
self.bids.append(bid)
self.current_bid = amount
return True
def buyout(self) -> bool:
"""
Attempt to buy out the auction immediately.
Returns:
True if buyout is available and successful, False otherwise
"""
if not self.is_auction() or not self.buyout_price:
return False
if not self.is_active() or self.has_ended():
return False
self.current_bid = self.buyout_price
self.status = ListingStatus.SOLD
return True
def get_winning_bidder(self) -> Optional[Bid]:
"""
Get the current winning bid.
Returns:
Winning Bid or None if no bids
"""
if not self.bids:
return None
# Bids are added chronologically, last one is highest
return self.bids[-1]
def cancel_listing(self) -> bool:
"""
Cancel this listing (seller action).
Returns:
True if successfully cancelled, False if cannot be cancelled
"""
if not self.is_active():
return False
# Cannot cancel auction with bids
if self.is_auction() and self.bids:
return False
self.status = ListingStatus.REMOVED
return True
def complete_sale(self) -> None:
"""Mark listing as sold."""
self.status = ListingStatus.SOLD
def expire_listing(self) -> None:
"""Mark listing as expired."""
self.status = ListingStatus.EXPIRED
def to_dict(self) -> Dict[str, Any]:
"""Serialize listing to dictionary."""
return {
"listing_id": self.listing_id,
"seller_id": self.seller_id,
"character_id": self.character_id,
"item_data": self.item_data.to_dict(),
"listing_type": self.listing_type.value,
"status": self.status.value,
"created_at": self.created_at,
"price": self.price,
"starting_bid": self.starting_bid,
"current_bid": self.current_bid,
"buyout_price": self.buyout_price,
"bids": [bid.to_dict() for bid in self.bids],
"auction_end": self.auction_end,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'MarketplaceListing':
"""Deserialize listing from dictionary."""
item_data = Item.from_dict(data["item_data"])
listing_type = ListingType(data["listing_type"])
status = ListingStatus(data.get("status", "active"))
bids = [Bid.from_dict(b) for b in data.get("bids", [])]
return cls(
listing_id=data["listing_id"],
seller_id=data["seller_id"],
character_id=data["character_id"],
item_data=item_data,
listing_type=listing_type,
status=status,
created_at=data.get("created_at", ""),
price=data.get("price", 0),
starting_bid=data.get("starting_bid", 0),
current_bid=data.get("current_bid", 0),
buyout_price=data.get("buyout_price", 0),
bids=bids,
auction_end=data.get("auction_end", ""),
)
@dataclass
class Transaction:
"""
Record of a completed transaction.
Tracks all sales for auditing and analytics.
Attributes:
transaction_id: Unique identifier
buyer_id: User ID of buyer
seller_id: User ID of seller
listing_id: Marketplace listing ID (if from marketplace)
item_data: Item that was sold
price: Final sale price in gold
timestamp: ISO timestamp of transaction
transaction_type: "marketplace_sale", "shop_purchase", etc.
"""
transaction_id: str
buyer_id: str
seller_id: str
item_data: Item
price: int
transaction_type: str
listing_id: str = ""
timestamp: str = ""
def __post_init__(self):
"""Initialize timestamp if not provided."""
if not self.timestamp:
self.timestamp = datetime.utcnow().isoformat()
def to_dict(self) -> Dict[str, Any]:
"""Serialize transaction to dictionary."""
return {
"transaction_id": self.transaction_id,
"buyer_id": self.buyer_id,
"seller_id": self.seller_id,
"listing_id": self.listing_id,
"item_data": self.item_data.to_dict(),
"price": self.price,
"timestamp": self.timestamp,
"transaction_type": self.transaction_type,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Transaction':
"""Deserialize transaction from dictionary."""
item_data = Item.from_dict(data["item_data"])
return cls(
transaction_id=data["transaction_id"],
buyer_id=data["buyer_id"],
seller_id=data["seller_id"],
listing_id=data.get("listing_id", ""),
item_data=item_data,
price=data["price"],
timestamp=data.get("timestamp", ""),
transaction_type=data["transaction_type"],
)
@dataclass
class ShopItem:
"""
Item sold by NPC shops.
Attributes:
item_id: Item identifier
item: Item details
stock: Available quantity (-1 = unlimited)
price: Fixed gold price
"""
item_id: str
item: Item
stock: int = -1 # -1 = unlimited
price: int = 0
def is_in_stock(self) -> bool:
"""Check if item is available for purchase."""
return self.stock != 0
def purchase(self, quantity: int = 1) -> bool:
"""
Attempt to purchase from stock.
Args:
quantity: Number of items to purchase
Returns:
True if purchase successful, False if insufficient stock
"""
if self.stock == -1: # Unlimited stock
return True
if self.stock < quantity:
return False
self.stock -= quantity
return True
def restock(self, quantity: int) -> None:
"""
Add stock to this shop item.
Args:
quantity: Amount to add to stock
"""
if self.stock == -1: # Unlimited, no need to restock
return
self.stock += quantity
def to_dict(self) -> Dict[str, Any]:
"""Serialize shop item to dictionary."""
return {
"item_id": self.item_id,
"item": self.item.to_dict(),
"stock": self.stock,
"price": self.price,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ShopItem':
"""Deserialize shop item from dictionary."""
item = Item.from_dict(data["item"])
return cls(
item_id=data["item_id"],
item=item,
stock=data.get("stock", -1),
price=data.get("price", 0),
)