Event layout page update.

This commit is contained in:
2026-01-11 15:21:17 -06:00
parent e0af183086
commit e50b2f31d3
13 changed files with 1460 additions and 183 deletions

View File

@ -6,6 +6,7 @@ from app.models.sport_event import SportEvent, SportType, EventStatus
from app.models.spread_bet import SpreadBet, SpreadBetStatus, TeamSide
from app.models.admin_settings import AdminSettings
from app.models.match_comment import MatchComment
from app.models.event_comment import EventComment
from app.models.gamification import (
UserStats,
Achievement,
@ -40,6 +41,7 @@ __all__ = [
"TeamSide",
"AdminSettings",
"MatchComment",
"EventComment",
# Gamification
"UserStats",
"Achievement",

View File

@ -0,0 +1,18 @@
from sqlalchemy import String, DateTime, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from app.database import Base
class EventComment(Base):
__tablename__ = "event_comments"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
event_id: Mapped[int] = mapped_column(ForeignKey("sport_events.id"))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
content: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationships
event: Mapped["SportEvent"] = relationship()
user: Mapped["User"] = relationship()

View File

@ -6,9 +6,11 @@ from typing import List
from datetime import datetime
from app.database import get_db
from app.models import User, SportEvent, SpreadBet, AdminSettings, EventStatus, SpreadBetStatus, TeamSide
from app.models import User, SportEvent, SpreadBet, AdminSettings, EventStatus, SpreadBetStatus, TeamSide, EventComment
from app.schemas.sport_event import SportEvent as SportEventSchema, SportEventWithBets
from app.schemas.event_comment import EventComment as EventCommentSchema, EventCommentCreate, EventCommentsResponse
from app.routers.auth import get_current_user
from app.routers.websocket import broadcast_to_event
router = APIRouter(prefix="/api/v1/sport-events", tags=["sport-events"])
@ -220,3 +222,104 @@ async def get_event_with_grid(
"updated_at": event.updated_at.isoformat(),
"spread_grid": spread_grid
}
@router.get("/{event_id}/comments", response_model=EventCommentsResponse)
async def get_event_comments(
event_id: int,
skip: int = 0,
limit: int = 50,
db: AsyncSession = Depends(get_db)
):
"""Get comments for an event - public access."""
# Verify event exists
event_result = await db.execute(
select(SportEvent).where(SportEvent.id == event_id)
)
event = event_result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Get comments with user info
result = await db.execute(
select(EventComment)
.options(selectinload(EventComment.user))
.where(EventComment.event_id == event_id)
.order_by(EventComment.created_at.asc())
.offset(skip)
.limit(limit)
)
comments = result.scalars().all()
# Get total count
count_result = await db.execute(
select(EventComment).where(EventComment.event_id == event_id)
)
total = len(count_result.scalars().all())
return EventCommentsResponse(
comments=[
EventCommentSchema(
id=c.id,
event_id=c.event_id,
user_id=c.user_id,
username=c.user.username,
content=c.content,
created_at=c.created_at
)
for c in comments
],
total=total
)
@router.post("/{event_id}/comments", response_model=EventCommentSchema)
async def add_event_comment(
event_id: int,
comment_data: EventCommentCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Add a comment to an event - requires authentication."""
# Verify event exists
event_result = await db.execute(
select(SportEvent).where(SportEvent.id == event_id)
)
event = event_result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Create comment
comment = EventComment(
event_id=event_id,
user_id=current_user.id,
content=comment_data.content
)
db.add(comment)
await db.commit()
await db.refresh(comment)
comment_response = EventCommentSchema(
id=comment.id,
event_id=comment.event_id,
user_id=comment.user_id,
username=current_user.username,
content=comment.content,
created_at=comment.created_at
)
# Broadcast new comment to event subscribers
await broadcast_to_event(
event_id,
"new_comment",
{
"id": comment.id,
"event_id": comment.event_id,
"user_id": comment.user_id,
"username": current_user.username,
"content": comment.content,
"created_at": comment.created_at.isoformat()
}
)
return comment_response

View File

@ -0,0 +1,24 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import List
class EventCommentCreate(BaseModel):
content: str = Field(..., min_length=1, max_length=500)
class EventComment(BaseModel):
id: int
event_id: int
user_id: int
username: str
content: str
created_at: datetime
class Config:
from_attributes = True
class EventCommentsResponse(BaseModel):
comments: List[EventComment]
total: int

546
backend/manage_events.py Executable file
View File

@ -0,0 +1,546 @@
#!/usr/bin/env python3
"""
Event Management Script
Manages sport events including:
- Creating new upcoming events
- Updating events to LIVE status
- Simulating live score updates
- Completing events and settling bets
Usage:
python manage_events.py [--create N] [--update] [--settle] [--continuous] [--delay SECONDS]
"""
import asyncio
import argparse
import random
from decimal import Decimal
from datetime import datetime, timedelta
from sqlalchemy import select, and_
from sqlalchemy.orm import selectinload
from app.database import async_session, init_db
from app.models import (
User, Wallet, SportEvent, SpreadBet, Transaction,
SportType, EventStatus, SpreadBetStatus, TeamSide,
TransactionType, TransactionStatus
)
# Team data by sport
TEAMS = {
SportType.FOOTBALL: {
"NFL": [
("Kansas City Chiefs", "Arrowhead Stadium"),
("San Francisco 49ers", "Levi's Stadium"),
("Philadelphia Eagles", "Lincoln Financial Field"),
("Dallas Cowboys", "AT&T Stadium"),
("Buffalo Bills", "Highmark Stadium"),
("Miami Dolphins", "Hard Rock Stadium"),
("Baltimore Ravens", "M&T Bank Stadium"),
("Detroit Lions", "Ford Field"),
("Green Bay Packers", "Lambeau Field"),
("Seattle Seahawks", "Lumen Field"),
("New York Giants", "MetLife Stadium"),
("Los Angeles Rams", "SoFi Stadium"),
("Cincinnati Bengals", "Paycor Stadium"),
("Jacksonville Jaguars", "TIAA Bank Field"),
("Minnesota Vikings", "U.S. Bank Stadium"),
("New Orleans Saints", "Caesars Superdome"),
],
"NCAA": [
("Alabama Crimson Tide", "Bryant-Denny Stadium"),
("Georgia Bulldogs", "Sanford Stadium"),
("Ohio State Buckeyes", "Ohio Stadium"),
("Michigan Wolverines", "Michigan Stadium"),
("Texas Longhorns", "Darrell K Royal Stadium"),
("USC Trojans", "Los Angeles Memorial Coliseum"),
("Clemson Tigers", "Memorial Stadium"),
("Penn State Nittany Lions", "Beaver Stadium"),
("Oregon Ducks", "Autzen Stadium"),
("Florida State Seminoles", "Doak Campbell Stadium"),
]
},
SportType.BASKETBALL: {
"NBA": [
("Los Angeles Lakers", "Crypto.com Arena"),
("Boston Celtics", "TD Garden"),
("Golden State Warriors", "Chase Center"),
("Milwaukee Bucks", "Fiserv Forum"),
("Phoenix Suns", "Footprint Center"),
("Denver Nuggets", "Ball Arena"),
("Miami Heat", "Kaseya Center"),
("Philadelphia 76ers", "Wells Fargo Center"),
("Brooklyn Nets", "Barclays Center"),
("Dallas Mavericks", "American Airlines Center"),
("New York Knicks", "Madison Square Garden"),
("Cleveland Cavaliers", "Rocket Mortgage FieldHouse"),
],
"NCAA": [
("Duke Blue Devils", "Cameron Indoor Stadium"),
("Kentucky Wildcats", "Rupp Arena"),
("Kansas Jayhawks", "Allen Fieldhouse"),
("North Carolina Tar Heels", "Dean E. Smith Center"),
("UCLA Bruins", "Pauley Pavilion"),
("Gonzaga Bulldogs", "McCarthey Athletic Center"),
("Villanova Wildcats", "Finneran Pavilion"),
("Arizona Wildcats", "McKale Center"),
]
},
SportType.BASEBALL: {
"MLB": [
("New York Yankees", "Yankee Stadium"),
("Los Angeles Dodgers", "Dodger Stadium"),
("Boston Red Sox", "Fenway Park"),
("Chicago Cubs", "Wrigley Field"),
("Atlanta Braves", "Truist Park"),
("Houston Astros", "Minute Maid Park"),
("San Diego Padres", "Petco Park"),
("Philadelphia Phillies", "Citizens Bank Park"),
("Texas Rangers", "Globe Life Field"),
("San Francisco Giants", "Oracle Park"),
]
},
SportType.HOCKEY: {
"NHL": [
("Vegas Golden Knights", "T-Mobile Arena"),
("Florida Panthers", "Amerant Bank Arena"),
("Edmonton Oilers", "Rogers Place"),
("Dallas Stars", "American Airlines Center"),
("Colorado Avalanche", "Ball Arena"),
("New York Rangers", "Madison Square Garden"),
("Boston Bruins", "TD Garden"),
("Carolina Hurricanes", "PNC Arena"),
("Toronto Maple Leafs", "Scotiabank Arena"),
("Tampa Bay Lightning", "Amalie Arena"),
]
},
SportType.SOCCER: {
"EPL": [
("Manchester City", "Etihad Stadium"),
("Arsenal", "Emirates Stadium"),
("Liverpool", "Anfield"),
("Manchester United", "Old Trafford"),
("Chelsea", "Stamford Bridge"),
("Tottenham Hotspur", "Tottenham Hotspur Stadium"),
("Newcastle United", "St. James' Park"),
("Brighton", "American Express Stadium"),
],
"MLS": [
("Inter Miami", "Chase Stadium"),
("LA Galaxy", "Dignity Health Sports Park"),
("LAFC", "BMO Stadium"),
("Atlanta United", "Mercedes-Benz Stadium"),
("Seattle Sounders", "Lumen Field"),
("New York Red Bulls", "Red Bull Arena"),
]
}
}
def get_random_matchup(sport: SportType) -> tuple:
"""Get a random matchup for a sport."""
leagues = TEAMS.get(sport, {})
if not leagues:
return None
league = random.choice(list(leagues.keys()))
teams = leagues[league]
# Pick two different teams
home_team, home_venue = random.choice(teams)
away_team, _ = random.choice([t for t in teams if t[0] != home_team])
return {
"sport": sport,
"league": league,
"home_team": home_team,
"away_team": away_team,
"venue": home_venue
}
def generate_spread(sport: SportType) -> float:
"""Generate a realistic spread for a sport."""
if sport == SportType.FOOTBALL:
# NFL/NCAA spreads typically -14 to +14
spread = round(random.uniform(-10, 10) * 2) / 2
elif sport == SportType.BASKETBALL:
# NBA spreads can be larger
spread = round(random.uniform(-12, 12) * 2) / 2
elif sport == SportType.BASEBALL:
# Baseball run lines are typically 1.5
spread = random.choice([-1.5, 1.5, -2.5, 2.5])
elif sport == SportType.HOCKEY:
# Hockey puck lines are typically 1.5
spread = random.choice([-1.5, 1.5, -2.5, 2.5])
elif sport == SportType.SOCCER:
# Soccer spreads are typically small
spread = random.choice([-0.5, 0.5, -1, 1, -1.5, 1.5])
else:
spread = round(random.uniform(-7, 7) * 2) / 2
return spread
async def create_new_events(db, admin_user_id: int, count: int = 5) -> list[SportEvent]:
"""Create new upcoming events."""
events = []
for _ in range(count):
sport = random.choice(list(SportType))
matchup = get_random_matchup(sport)
if not matchup:
continue
# Random game time in the next 1-7 days
hours_ahead = random.randint(1, 168) # 1 hour to 7 days
game_time = datetime.utcnow() + timedelta(hours=hours_ahead)
spread = generate_spread(sport)
event = SportEvent(
sport=matchup["sport"],
home_team=matchup["home_team"],
away_team=matchup["away_team"],
official_spread=spread,
game_time=game_time,
venue=matchup["venue"],
league=matchup["league"],
min_spread=spread - 5,
max_spread=spread + 5,
min_bet_amount=10.0,
max_bet_amount=1000.0,
status=EventStatus.UPCOMING,
created_by=admin_user_id
)
db.add(event)
events.append(event)
await db.commit()
for event in events:
print(f" [NEW] {event.home_team} vs {event.away_team} ({event.league}) - {event.game_time.strftime('%m/%d %H:%M')}")
return events
async def update_events_to_live(db) -> list[SportEvent]:
"""Update events that should be live (game time has passed)."""
# Find upcoming events where game time has passed
result = await db.execute(
select(SportEvent).where(
and_(
SportEvent.status == EventStatus.UPCOMING,
SportEvent.game_time <= datetime.utcnow()
)
)
)
events = result.scalars().all()
for event in events:
event.status = EventStatus.LIVE
event.final_score_home = 0
event.final_score_away = 0
print(f" [LIVE] {event.home_team} vs {event.away_team} is now LIVE!")
await db.commit()
return list(events)
async def update_live_scores(db) -> list[SportEvent]:
"""Update scores for live events."""
result = await db.execute(
select(SportEvent).where(SportEvent.status == EventStatus.LIVE)
)
events = result.scalars().all()
for event in events:
# Randomly add points based on sport
if event.sport == SportType.FOOTBALL:
# Football scoring: 3 or 7 points typically
if random.random() < 0.3:
scorer = random.choice(['home', 'away'])
points = random.choice([3, 7, 6, 2])
if scorer == 'home':
event.final_score_home = (event.final_score_home or 0) + points
else:
event.final_score_away = (event.final_score_away or 0) + points
print(f" [SCORE] {event.home_team} {event.final_score_home} - {event.final_score_away} {event.away_team}")
elif event.sport == SportType.BASKETBALL:
# Basketball: 2 or 3 points frequently
if random.random() < 0.5:
scorer = random.choice(['home', 'away'])
points = random.choice([2, 2, 2, 3, 1])
if scorer == 'home':
event.final_score_home = (event.final_score_home or 0) + points
else:
event.final_score_away = (event.final_score_away or 0) + points
print(f" [SCORE] {event.home_team} {event.final_score_home} - {event.final_score_away} {event.away_team}")
elif event.sport in [SportType.BASEBALL, SportType.HOCKEY, SportType.SOCCER]:
# Low scoring sports
if random.random() < 0.15:
scorer = random.choice(['home', 'away'])
if scorer == 'home':
event.final_score_home = (event.final_score_home or 0) + 1
else:
event.final_score_away = (event.final_score_away or 0) + 1
print(f" [SCORE] {event.home_team} {event.final_score_home} - {event.final_score_away} {event.away_team}")
await db.commit()
return list(events)
async def complete_events(db) -> list[SportEvent]:
"""Complete live events that have been running long enough."""
result = await db.execute(
select(SportEvent).where(SportEvent.status == EventStatus.LIVE)
)
events = result.scalars().all()
completed = []
for event in events:
# Calculate "game duration" based on when it went live (using updated_at as proxy)
# Complete events after ~10 iterations (simulated game time)
game_duration = (datetime.utcnow() - event.updated_at).total_seconds()
# 20% chance to complete if scores are reasonable
home_score = event.final_score_home or 0
away_score = event.final_score_away or 0
should_complete = False
if event.sport == SportType.FOOTBALL and (home_score + away_score) >= 20:
should_complete = random.random() < 0.3
elif event.sport == SportType.BASKETBALL and (home_score + away_score) >= 150:
should_complete = random.random() < 0.3
elif event.sport in [SportType.BASEBALL, SportType.HOCKEY] and (home_score + away_score) >= 5:
should_complete = random.random() < 0.3
elif event.sport == SportType.SOCCER and (home_score + away_score) >= 2:
should_complete = random.random() < 0.3
if should_complete:
event.status = EventStatus.COMPLETED
completed.append(event)
print(f" [FINAL] {event.home_team} {home_score} - {away_score} {event.away_team}")
await db.commit()
return completed
async def settle_bets(db, completed_events: list[SportEvent]) -> int:
"""Settle bets for completed events."""
settled_count = 0
for event in completed_events:
# Get all matched bets for this event
result = await db.execute(
select(SpreadBet)
.options(selectinload(SpreadBet.creator), selectinload(SpreadBet.taker))
.where(
and_(
SpreadBet.event_id == event.id,
SpreadBet.status == SpreadBetStatus.MATCHED
)
)
)
bets = result.scalars().all()
for bet in bets:
# Calculate spread result
home_score = event.final_score_home or 0
away_score = event.final_score_away or 0
actual_spread = home_score - away_score # Positive = home won by X
# Creator's pick
if bet.team == TeamSide.HOME:
# Creator bet on home team with the spread
# Home team needs to "cover" - actual margin > bet spread
creator_wins = actual_spread > bet.spread
else:
# Creator bet on away team
# Away team needs to cover - actual margin < bet spread (inverted)
creator_wins = actual_spread < -bet.spread
# Get wallets
creator_wallet_result = await db.execute(
select(Wallet).where(Wallet.user_id == bet.creator_id)
)
creator_wallet = creator_wallet_result.scalar_one_or_none()
taker_wallet_result = await db.execute(
select(Wallet).where(Wallet.user_id == bet.taker_id)
)
taker_wallet = taker_wallet_result.scalar_one_or_none()
if not creator_wallet or not taker_wallet:
continue
# Calculate payout (total pot minus commission)
total_pot = bet.stake_amount * 2
commission = total_pot * (bet.house_commission_percent / 100)
payout = total_pot - commission
winner = bet.creator if creator_wins else bet.taker
winner_wallet = creator_wallet if creator_wins else taker_wallet
loser_wallet = taker_wallet if creator_wins else creator_wallet
# Release escrow and pay winner
creator_wallet.escrow -= bet.stake_amount
taker_wallet.escrow -= bet.stake_amount
winner_wallet.balance += payout
# Create payout transaction
payout_tx = Transaction(
user_id=winner.id,
wallet_id=winner_wallet.id,
type=TransactionType.BET_WON,
amount=payout,
balance_after=winner_wallet.balance,
reference_id=bet.id,
description=f"Won spread bet #{bet.id}",
status=TransactionStatus.COMPLETED
)
db.add(payout_tx)
# Update bet
bet.status = SpreadBetStatus.COMPLETED
bet.winner_id = winner.id
bet.payout_amount = payout
bet.completed_at = datetime.utcnow()
settled_count += 1
print(f" [SETTLED] Bet #{bet.id}: {winner.username} wins ${payout:.2f}")
await db.commit()
return settled_count
async def run_event_management(
create_count: int = 0,
do_update: bool = False,
do_settle: bool = False,
continuous: bool = False,
delay: float = 5.0
):
"""Run the event management script."""
print("=" * 60)
print("H2H Event Manager")
print("=" * 60)
await init_db()
iteration = 0
while True:
iteration += 1
print(f"\n--- Iteration {iteration} ---")
async with async_session() as db:
# Get or create admin user
admin_result = await db.execute(
select(User).where(User.is_admin == True)
)
admin = admin_result.scalar_one_or_none()
if not admin:
# Use first user as admin
user_result = await db.execute(select(User).limit(1))
admin = user_result.scalar_one_or_none()
if not admin:
print(" No users found. Run seed_data.py first.")
if not continuous:
break
await asyncio.sleep(delay)
continue
# Create new events
if create_count > 0 or continuous:
events_to_create = create_count if create_count > 0 else random.randint(1, 3)
if continuous and random.random() < 0.3: # 30% chance in continuous mode
print(f"\n Creating {events_to_create} new events...")
await create_new_events(db, admin.id, events_to_create)
elif create_count > 0:
print(f"\n Creating {events_to_create} new events...")
await create_new_events(db, admin.id, events_to_create)
# Update events
if do_update or continuous:
print("\n Checking for events to update...")
# Move upcoming to live
await update_events_to_live(db)
# Update live scores
await update_live_scores(db)
# Complete events
completed = await complete_events(db)
# Settle bets for completed events
if (do_settle or continuous) and completed:
print(f"\n Settling bets for {len(completed)} completed events...")
settled = await settle_bets(db, completed)
print(f" Settled {settled} bets")
if not continuous:
break
await asyncio.sleep(delay)
print("\n" + "=" * 60)
print("Event management complete!")
print("=" * 60)
def main():
parser = argparse.ArgumentParser(description="Manage H2H sport events")
parser.add_argument(
"--create", "-c",
type=int,
default=0,
help="Number of new events to create (default: 0)"
)
parser.add_argument(
"--update", "-u",
action="store_true",
help="Update event statuses and scores"
)
parser.add_argument(
"--settle", "-s",
action="store_true",
help="Settle bets for completed events"
)
parser.add_argument(
"--continuous",
action="store_true",
help="Run continuously until interrupted"
)
parser.add_argument(
"--delay", "-d",
type=float,
default=5.0,
help="Delay between iterations in seconds (default: 5.0)"
)
args = parser.parse_args()
# If no specific action, just create events
if not args.create and not args.update and not args.settle and not args.continuous:
args.create = 5 # Default: create 5 events
try:
asyncio.run(run_event_management(
create_count=args.create,
do_update=args.update,
do_settle=args.settle,
continuous=args.continuous,
delay=args.delay
))
except KeyboardInterrupt:
print("\n\nEvent management stopped by user.")
if __name__ == "__main__":
main()

436
backend/simulate_activity.py Executable file
View File

@ -0,0 +1,436 @@
#!/usr/bin/env python3
"""
Activity Simulation Script
Simulates random user activity including:
- New user registrations
- Creating spread bets
- Taking/matching bets
- Adding comments to events and matches
- Cancelling bets
Usage:
python simulate_activity.py [--iterations N] [--delay SECONDS] [--continuous]
"""
import asyncio
import argparse
import random
from decimal import Decimal
from datetime import datetime
from sqlalchemy import select, and_
from sqlalchemy.orm import selectinload
from app.database import async_session, init_db
from app.models import (
User, Wallet, SportEvent, SpreadBet, EventComment, MatchComment,
EventStatus, SpreadBetStatus, TeamSide, Transaction, TransactionType, TransactionStatus
)
from app.utils.security import get_password_hash
# Sample data for generating random users
FIRST_NAMES = [
"James", "Emma", "Liam", "Olivia", "Noah", "Ava", "Oliver", "Sophia",
"Elijah", "Isabella", "Lucas", "Mia", "Mason", "Charlotte", "Ethan",
"Amelia", "Alexander", "Harper", "Henry", "Evelyn", "Sebastian", "Luna",
"Jack", "Camila", "Aiden", "Gianna", "Owen", "Abigail", "Samuel", "Ella",
"Ryan", "Scarlett", "Nathan", "Emily", "Caleb", "Elizabeth", "Hunter",
"Sofia", "Christian", "Avery", "Landon", "Chloe", "Jonathan", "Victoria"
]
LAST_NAMES = [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller",
"Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez",
"Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
"Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark",
"Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", "King"
]
# Sample comments for events
EVENT_COMMENTS = [
"This is going to be a great game!",
"Home team looking strong this season",
"I'm betting on the underdog here",
"What do you all think about the spread?",
"Last time these teams played it was close",
"Weather might be a factor today",
"Key player is out, that changes everything",
"The odds seem off to me",
"Anyone else feeling bullish on the away team?",
"This matchup is always entertaining",
"Line moved a lot overnight",
"Sharp money coming in on the home side",
"Public is heavy on the favorite",
"Value play on the underdog here",
"Injury report looking concerning",
"Home field advantage is huge here",
"Expecting a low-scoring affair",
"Over/under seems too high",
"Rivalry game, throw out the records!",
"Coach has a great record against this opponent"
]
# Sample comments for matches (between two bettors)
MATCH_COMMENTS = [
"Good luck!",
"May the best bettor win",
"This should be interesting",
"I'm feeling confident about this one",
"Let's see how this plays out",
"Nice bet, looking forward to the game",
"GL HF",
"First time betting against you",
"Rematch from last week!",
"I've been waiting for this matchup",
"Your team doesn't stand a chance!",
"We'll see about that...",
"Close game incoming",
"I'll be watching every play",
"Don't count your winnings yet!"
]
async def create_random_user(db) -> User | None:
"""Create a new random user with a wallet."""
first = random.choice(FIRST_NAMES)
last = random.choice(LAST_NAMES)
# Generate unique username
suffix = random.randint(100, 9999)
username = f"{first.lower()}{last.lower()}{suffix}"
email = f"{username}@example.com"
# Check if user already exists
existing = await db.execute(
select(User).where(User.username == username)
)
if existing.scalar_one_or_none():
return None
user = User(
email=email,
username=username,
password_hash=get_password_hash("password123"),
display_name=f"{first} {last}"
)
db.add(user)
await db.flush()
# Create wallet with random starting balance
starting_balance = Decimal(str(random.randint(500, 5000)))
wallet = Wallet(
user_id=user.id,
balance=starting_balance,
escrow=Decimal("0.00")
)
db.add(wallet)
await db.commit()
print(f" [USER] Created user: {user.username} with ${starting_balance} balance")
return user
async def create_random_bet(db, users: list[User], events: list[SportEvent]) -> SpreadBet | None:
"""Create a random spread bet on an event."""
if not users or not events:
return None
# Filter for users with sufficient balance
users_with_balance = []
for user in users:
wallet_result = await db.execute(
select(Wallet).where(Wallet.user_id == user.id)
)
wallet = wallet_result.scalar_one_or_none()
if wallet and wallet.balance >= Decimal("10"):
users_with_balance.append((user, wallet))
if not users_with_balance:
return None
user, wallet = random.choice(users_with_balance)
event = random.choice(events)
# Random spread within event range
spread = round(random.uniform(event.min_spread, event.max_spread) * 2) / 2 # Round to 0.5
# Random stake (10-50% of balance, max $500)
max_stake = min(float(wallet.balance) * 0.5, 500)
stake = Decimal(str(round(random.uniform(10, max(10, max_stake)), 2)))
# Random team selection
team = random.choice([TeamSide.HOME, TeamSide.AWAY])
bet = SpreadBet(
event_id=event.id,
spread=spread,
team=team,
creator_id=user.id,
stake_amount=stake,
house_commission_percent=Decimal("10.00"),
status=SpreadBetStatus.OPEN
)
db.add(bet)
await db.commit()
team_name = event.home_team if team == TeamSide.HOME else event.away_team
print(f" [BET] {user.username} created ${stake} bet on {team_name} {'+' if spread > 0 else ''}{spread}")
return bet
async def take_random_bet(db, users: list[User]) -> SpreadBet | None:
"""Have a random user take an open bet."""
# Get open bets
result = await db.execute(
select(SpreadBet)
.options(selectinload(SpreadBet.event), selectinload(SpreadBet.creator))
.where(SpreadBet.status == SpreadBetStatus.OPEN)
)
open_bets = result.scalars().all()
if not open_bets:
return None
bet = random.choice(open_bets)
# Find eligible takers (not creator, has balance)
eligible_takers = []
for user in users:
if user.id == bet.creator_id:
continue
wallet_result = await db.execute(
select(Wallet).where(Wallet.user_id == user.id)
)
wallet = wallet_result.scalar_one_or_none()
if wallet and wallet.balance >= bet.stake_amount:
eligible_takers.append((user, wallet))
if not eligible_takers:
return None
taker, taker_wallet = random.choice(eligible_takers)
# Get creator's wallet
creator_wallet_result = await db.execute(
select(Wallet).where(Wallet.user_id == bet.creator_id)
)
creator_wallet = creator_wallet_result.scalar_one_or_none()
if not creator_wallet or creator_wallet.balance < bet.stake_amount:
return None
# Lock funds for both parties
creator_wallet.balance -= bet.stake_amount
creator_wallet.escrow += bet.stake_amount
taker_wallet.balance -= bet.stake_amount
taker_wallet.escrow += bet.stake_amount
# Create transactions
creator_tx = Transaction(
user_id=bet.creator_id,
wallet_id=creator_wallet.id,
type=TransactionType.ESCROW_LOCK,
amount=-bet.stake_amount,
balance_after=creator_wallet.balance,
reference_id=bet.id,
description=f"Escrow lock for spread bet #{bet.id}",
status=TransactionStatus.COMPLETED
)
taker_tx = Transaction(
user_id=taker.id,
wallet_id=taker_wallet.id,
type=TransactionType.ESCROW_LOCK,
amount=-bet.stake_amount,
balance_after=taker_wallet.balance,
reference_id=bet.id,
description=f"Escrow lock for spread bet #{bet.id}",
status=TransactionStatus.COMPLETED
)
db.add(creator_tx)
db.add(taker_tx)
# Update bet
bet.taker_id = taker.id
bet.status = SpreadBetStatus.MATCHED
bet.matched_at = datetime.utcnow()
await db.commit()
print(f" [MATCH] {taker.username} took {bet.creator.username}'s ${bet.stake_amount} bet")
return bet
async def cancel_random_bet(db) -> SpreadBet | None:
"""Cancel a random open bet."""
result = await db.execute(
select(SpreadBet)
.options(selectinload(SpreadBet.creator))
.where(SpreadBet.status == SpreadBetStatus.OPEN)
)
open_bets = result.scalars().all()
if not open_bets:
return None
# 20% chance to cancel
if random.random() > 0.2:
return None
bet = random.choice(open_bets)
bet.status = SpreadBetStatus.CANCELLED
await db.commit()
print(f" [CANCEL] {bet.creator.username} cancelled their ${bet.stake_amount} bet")
return bet
async def add_event_comment(db, users: list[User], events: list[SportEvent]) -> EventComment | None:
"""Add a random comment to an event."""
if not users or not events:
return None
user = random.choice(users)
event = random.choice(events)
content = random.choice(EVENT_COMMENTS)
comment = EventComment(
event_id=event.id,
user_id=user.id,
content=content
)
db.add(comment)
await db.commit()
print(f" [COMMENT] {user.username} on {event.home_team} vs {event.away_team}: \"{content[:40]}...\"")
return comment
async def add_match_comment(db, users: list[User]) -> MatchComment | None:
"""Add a random comment to a matched bet."""
# Get matched bets
result = await db.execute(
select(SpreadBet)
.options(selectinload(SpreadBet.creator), selectinload(SpreadBet.taker))
.where(SpreadBet.status == SpreadBetStatus.MATCHED)
)
matched_bets = result.scalars().all()
if not matched_bets:
return None
bet = random.choice(matched_bets)
# Comment from either creator or taker
user = random.choice([bet.creator, bet.taker])
content = random.choice(MATCH_COMMENTS)
comment = MatchComment(
spread_bet_id=bet.id,
user_id=user.id,
content=content
)
db.add(comment)
await db.commit()
print(f" [CHAT] {user.username} in match #{bet.id}: \"{content}\"")
return comment
async def run_simulation(iterations: int = 10, delay: float = 2.0, continuous: bool = False):
"""Run the activity simulation."""
print("=" * 60)
print("H2H Activity Simulator")
print("=" * 60)
await init_db()
iteration = 0
while continuous or iteration < iterations:
iteration += 1
print(f"\n--- Iteration {iteration} ---")
async with async_session() as db:
# Get existing users and events
users_result = await db.execute(select(User))
users = list(users_result.scalars().all())
events_result = await db.execute(
select(SportEvent).where(SportEvent.status == EventStatus.UPCOMING)
)
events = list(events_result.scalars().all())
if not events:
print(" No upcoming events found. Run manage_events.py first.")
if not continuous:
break
await asyncio.sleep(delay)
continue
# Random actions with weighted probabilities
actions = [
(create_random_user, 0.15), # 15% - Create user
(lambda db: create_random_bet(db, users, events), 0.30), # 30% - Create bet
(lambda db: take_random_bet(db, users), 0.20), # 20% - Take bet
(lambda db: cancel_random_bet(db), 0.05), # 5% - Cancel bet
(lambda db: add_event_comment(db, users, events), 0.15), # 15% - Event comment
(lambda db: add_match_comment(db, users), 0.15), # 15% - Match comment
]
# Perform 1-3 random actions per iteration
num_actions = random.randint(1, 3)
for _ in range(num_actions):
# Weighted random selection
rand = random.random()
cumulative = 0
for action_fn, probability in actions:
cumulative += probability
if rand <= cumulative:
try:
if action_fn == create_random_user:
await action_fn(db)
else:
await action_fn(db)
except Exception as e:
print(f" [ERROR] {e}")
break
await asyncio.sleep(delay)
print("\n" + "=" * 60)
print("Simulation complete!")
print("=" * 60)
def main():
parser = argparse.ArgumentParser(description="Simulate H2H platform activity")
parser.add_argument(
"--iterations", "-n",
type=int,
default=10,
help="Number of simulation iterations (default: 10)"
)
parser.add_argument(
"--delay", "-d",
type=float,
default=2.0,
help="Delay between iterations in seconds (default: 2.0)"
)
parser.add_argument(
"--continuous", "-c",
action="store_true",
help="Run continuously until interrupted"
)
args = parser.parse_args()
try:
asyncio.run(run_simulation(
iterations=args.iterations,
delay=args.delay,
continuous=args.continuous
))
except KeyboardInterrupt:
print("\n\nSimulation stopped by user.")
if __name__ == "__main__":
main()