Merge branch 'main' of git.gade.gg:NikolajDanger/wiki-tcg
This commit is contained in:
349
backend/ai.py
Normal file
349
backend/ai.py
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
from enum import Enum
|
||||||
|
from card import Card
|
||||||
|
from game import action_play_card, action_sacrifice, action_end_turn, BOARD_SIZE
|
||||||
|
|
||||||
|
AI_USER_ID = "ai"
|
||||||
|
|
||||||
|
class AIPersonality(Enum):
|
||||||
|
AGGRESSIVE = "aggressive" # Prefers high attack cards, plays aggressively
|
||||||
|
DEFENSIVE = "defensive" # Prefers high defense cards, plays conservatively
|
||||||
|
BALANCED = "balanced" # Mix of offense and defense
|
||||||
|
GREEDY = "greedy" # Prioritizes high cost cards, willing to sacrifice
|
||||||
|
SWARM = "swarm" # Prefers low cost cards, fills board quickly
|
||||||
|
CONTROL = "control" # Focuses on board control and efficiency
|
||||||
|
ARBITRARY = "arbitrary" # Just does whatever
|
||||||
|
|
||||||
|
def get_random_personality() -> AIPersonality:
|
||||||
|
"""Returns a random AI personality."""
|
||||||
|
return random.choice(list(AIPersonality))
|
||||||
|
|
||||||
|
def calculate_exact_cost(attack: int, defense: int) -> float:
|
||||||
|
"""Calculate the exact cost before rounding (matches card.py formula)."""
|
||||||
|
return min(12.0, max(1.0, ((attack**2 + defense**2)**0.18) / 1.5))
|
||||||
|
|
||||||
|
def get_power_curve_value(card: Card) -> float:
|
||||||
|
"""
|
||||||
|
Returns how much "above the power curve" a card is.
|
||||||
|
Positive values mean the card is better than expected for its cost.
|
||||||
|
"""
|
||||||
|
exact_cost = calculate_exact_cost(card.attack, card.defense)
|
||||||
|
return exact_cost - card.cost
|
||||||
|
|
||||||
|
def get_card_efficiency(card: Card) -> float:
|
||||||
|
"""
|
||||||
|
Returns the total stats per cost ratio.
|
||||||
|
Higher is better (more stats for the cost).
|
||||||
|
"""
|
||||||
|
if card.cost == 0:
|
||||||
|
return 0
|
||||||
|
return (card.attack + card.defense) / card.cost
|
||||||
|
|
||||||
|
def score_card_for_personality(card: Card, personality: AIPersonality) -> float:
|
||||||
|
"""
|
||||||
|
Score a card based on how well it fits the AI personality.
|
||||||
|
Higher scores are better fits.
|
||||||
|
"""
|
||||||
|
if personality == AIPersonality.AGGRESSIVE:
|
||||||
|
# Prefer high attack, attack > defense
|
||||||
|
attack_bias = card.attack * 1.5
|
||||||
|
return attack_bias + (card.attack - card.defense) * 0.5
|
||||||
|
|
||||||
|
elif personality == AIPersonality.DEFENSIVE:
|
||||||
|
# Prefer high defense, defense > attack
|
||||||
|
defense_bias = card.defense * 1.5
|
||||||
|
return defense_bias + (card.defense - card.attack) * 0.5
|
||||||
|
|
||||||
|
elif personality == AIPersonality.BALANCED:
|
||||||
|
# Prefer balanced stats
|
||||||
|
stat_diff = abs(card.attack - card.defense)
|
||||||
|
balance_score = (card.attack + card.defense) - stat_diff * 0.3
|
||||||
|
return balance_score
|
||||||
|
|
||||||
|
elif personality == AIPersonality.GREEDY:
|
||||||
|
# Prefer high cost cards
|
||||||
|
return card.cost * 2 + (card.attack + card.defense) * 0.5
|
||||||
|
|
||||||
|
elif personality == AIPersonality.SWARM:
|
||||||
|
# Prefer low cost cards
|
||||||
|
low_cost_bonus = (13 - card.cost) * 1.5
|
||||||
|
return low_cost_bonus + (card.attack + card.defense) * 0.3
|
||||||
|
|
||||||
|
elif personality == AIPersonality.CONTROL:
|
||||||
|
# Prefer efficient cards (good stats per cost)
|
||||||
|
efficiency = get_card_efficiency(card)
|
||||||
|
total_stats = card.attack + card.defense
|
||||||
|
return efficiency * 5 + total_stats * 0.2
|
||||||
|
|
||||||
|
elif personality == AIPersonality.ARBITRARY:
|
||||||
|
# Does whatever
|
||||||
|
return random.random()*100
|
||||||
|
|
||||||
|
return card.attack + card.defense
|
||||||
|
|
||||||
|
def energy_curve(difficulty: int, personality: AIPersonality) -> tuple[int, int, int]:
|
||||||
|
"""Calculate a desired energy curve based on difficulty, personality, and a random factor"""
|
||||||
|
|
||||||
|
# First: cards with cost 1-3
|
||||||
|
# Second: cards with cost 4-6
|
||||||
|
# Third is inferred, and is cards with cost 7+
|
||||||
|
diff_low, diff_mid = [
|
||||||
|
(12, 8), # 1
|
||||||
|
(11, 9), # 2
|
||||||
|
(10, 9), # 3
|
||||||
|
( 9,10), # 4
|
||||||
|
( 9, 9), # 5
|
||||||
|
( 9, 8), # 6
|
||||||
|
( 8, 9), # 7
|
||||||
|
( 7,10), # 8
|
||||||
|
( 7, 9), # 9
|
||||||
|
( 6, 9), # 10
|
||||||
|
][difficulty - 1]
|
||||||
|
|
||||||
|
r1 = random.randint(0,20)
|
||||||
|
r2 = random.randint(0,20-r1)
|
||||||
|
pers_low, pers_mid = {
|
||||||
|
AIPersonality.AGGRESSIVE: ( 8,10),
|
||||||
|
AIPersonality.ARBITRARY: (r1,r2),
|
||||||
|
AIPersonality.BALANCED: ( 7,10),
|
||||||
|
AIPersonality.CONTROL: ( 3, 8),
|
||||||
|
AIPersonality.DEFENSIVE: ( 6, 8),
|
||||||
|
AIPersonality.GREEDY: ( 3, 7),
|
||||||
|
AIPersonality.SWARM: (15, 3),
|
||||||
|
}[personality]
|
||||||
|
|
||||||
|
# Blend difficulty (70%) and personality (30%) curves
|
||||||
|
blended_low = diff_low * 0.7 + pers_low * 0.3
|
||||||
|
blended_mid = diff_mid * 0.7 + pers_mid * 0.3
|
||||||
|
|
||||||
|
# Add small random variance (±1)
|
||||||
|
low = int(blended_low + random.uniform(-1, 1))
|
||||||
|
mid = int(blended_mid + random.uniform(-1, 1))
|
||||||
|
|
||||||
|
# Ensure low + mid doesn't exceed 20
|
||||||
|
if low + mid > 20:
|
||||||
|
# Scale down proportionally
|
||||||
|
total = low + mid
|
||||||
|
low = int((low / total) * 20)
|
||||||
|
mid = 20 - low
|
||||||
|
high = 0
|
||||||
|
else:
|
||||||
|
high = 20 - low - mid
|
||||||
|
|
||||||
|
# Apply difficulty constraints
|
||||||
|
if difficulty == 1:
|
||||||
|
# Difficulty 1: absolutely no high-cost cards
|
||||||
|
if high > 0:
|
||||||
|
# Redistribute high cards to low and mid
|
||||||
|
low += high // 2
|
||||||
|
mid += high - (high // 2)
|
||||||
|
high = 0
|
||||||
|
|
||||||
|
# Final bounds checking
|
||||||
|
low = max(0, min(20, low))
|
||||||
|
mid = max(0, min(20 - low, mid))
|
||||||
|
high = max(0, 20 - low - mid)
|
||||||
|
|
||||||
|
return (low, mid, high)
|
||||||
|
|
||||||
|
def choose_cards(cards: list[Card], difficulty: int, personality: AIPersonality) -> list[Card]:
|
||||||
|
"""
|
||||||
|
Choose 20 cards from available cards based on difficulty and personality.
|
||||||
|
|
||||||
|
Difficulty (1-10) affects:
|
||||||
|
- Higher difficulty = prefers cards above the power curve
|
||||||
|
- Lower difficulty = prefers low-cost cards for early game playability
|
||||||
|
- Lower difficulty = avoids taking the ridiculously good high-cost cards
|
||||||
|
|
||||||
|
Personality affects which types of cards are preferred.
|
||||||
|
"""
|
||||||
|
if len(cards) < 20:
|
||||||
|
return cards
|
||||||
|
|
||||||
|
# Get target energy curve based on difficulty and personality
|
||||||
|
target_low, target_mid, target_high = energy_curve(difficulty, personality)
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
remaining = list(cards)
|
||||||
|
|
||||||
|
# Fill each cost bracket by distributing across individual cost levels
|
||||||
|
for cost_min, cost_max, target_count in [(1, 3, target_low), (4, 6, target_mid), (7, 12, target_high)]:
|
||||||
|
if target_count == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bracket_cards = [c for c in remaining if cost_min <= c.cost <= cost_max]
|
||||||
|
if not bracket_cards:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Group cards by exact cost
|
||||||
|
by_cost = {}
|
||||||
|
for card in bracket_cards:
|
||||||
|
if card.cost not in by_cost:
|
||||||
|
by_cost[card.cost] = []
|
||||||
|
by_cost[card.cost].append(card)
|
||||||
|
|
||||||
|
# Distribute target_count across available costs
|
||||||
|
available_costs = sorted(by_cost.keys())
|
||||||
|
if not available_costs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate how many cards to take from each cost level
|
||||||
|
per_cost = max(1, target_count // len(available_costs))
|
||||||
|
remainder = target_count % len(available_costs)
|
||||||
|
|
||||||
|
for cost in available_costs:
|
||||||
|
cost_cards = by_cost[cost]
|
||||||
|
# Score cards at this specific cost level
|
||||||
|
cost_scores = []
|
||||||
|
for card in cost_cards:
|
||||||
|
# Base score from personality (but normalize by cost to avoid bias)
|
||||||
|
personality_score = score_card_for_personality(card, personality)
|
||||||
|
# Normalize: divide by cost to make 1-cost and 3-cost comparable
|
||||||
|
# Then multiply by average cost in bracket for scaling
|
||||||
|
avg_bracket_cost = (cost_min + cost_max) / 2
|
||||||
|
normalized_score = (personality_score / max(1, card.cost)) * avg_bracket_cost
|
||||||
|
|
||||||
|
# Power curve bonus
|
||||||
|
power_curve = get_power_curve_value(card)
|
||||||
|
difficulty_factor = (difficulty - 5.5) / 4.5
|
||||||
|
power_curve_score = power_curve * difficulty_factor * 5
|
||||||
|
|
||||||
|
# For low difficulties, heavily penalize high-cost cards with good stats
|
||||||
|
if difficulty <= 4 and card.cost >= 7:
|
||||||
|
power_penalty = max(0, power_curve) * -10
|
||||||
|
normalized_score += power_penalty
|
||||||
|
|
||||||
|
total_score = normalized_score + power_curve_score
|
||||||
|
cost_scores.append((card, total_score))
|
||||||
|
|
||||||
|
# Sort and take best from this cost level
|
||||||
|
cost_scores.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
# Take per_cost, plus 1 extra if this is one of the remainder slots
|
||||||
|
to_take = per_cost
|
||||||
|
if remainder > 0:
|
||||||
|
to_take += 1
|
||||||
|
remainder -= 1
|
||||||
|
to_take = min(to_take, len(cost_scores))
|
||||||
|
|
||||||
|
for i in range(to_take):
|
||||||
|
card = cost_scores[i][0]
|
||||||
|
selected.append(card)
|
||||||
|
remaining.remove(card)
|
||||||
|
if len(selected) >= 20:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(selected) >= 20:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fill remaining slots with best available cards
|
||||||
|
# This handles cases where brackets didn't have enough cards
|
||||||
|
while len(selected) < 20 and remaining:
|
||||||
|
remaining_scores = []
|
||||||
|
for card in remaining:
|
||||||
|
personality_score = score_card_for_personality(card, personality)
|
||||||
|
power_curve = get_power_curve_value(card)
|
||||||
|
difficulty_factor = (difficulty - 5.5) / 4.5
|
||||||
|
power_curve_score = power_curve * difficulty_factor * 5
|
||||||
|
|
||||||
|
# For remaining slots, add a slight preference for lower cost cards
|
||||||
|
# to ensure we have early-game plays
|
||||||
|
cost_penalty = (card.cost - 4) * 0.5 # Neutral at 4, penalty for higher
|
||||||
|
|
||||||
|
total_score = personality_score + power_curve_score - cost_penalty
|
||||||
|
remaining_scores.append((card, total_score))
|
||||||
|
|
||||||
|
remaining_scores.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
card = remaining_scores[0][0]
|
||||||
|
selected.append(card)
|
||||||
|
remaining.remove(card)
|
||||||
|
|
||||||
|
return selected[:20]
|
||||||
|
|
||||||
|
async def run_ai_turn(game_id: str):
|
||||||
|
from game_manager import (
|
||||||
|
active_games, connections, active_deck_ids,
|
||||||
|
serialize_state, record_game_result, calculate_combat_animation_time
|
||||||
|
)
|
||||||
|
|
||||||
|
state = active_games.get(game_id)
|
||||||
|
if not state or state.result:
|
||||||
|
return
|
||||||
|
if state.active_player_id != AI_USER_ID:
|
||||||
|
return
|
||||||
|
|
||||||
|
human_id = state.opponent_id(AI_USER_ID)
|
||||||
|
waited = 0
|
||||||
|
while not connections[game_id].get(human_id) and waited < 10:
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
waited += 0.5
|
||||||
|
|
||||||
|
await asyncio.sleep(calculate_combat_animation_time(state.last_combat_events))
|
||||||
|
|
||||||
|
player = state.players[AI_USER_ID]
|
||||||
|
|
||||||
|
ws = connections[game_id].get(human_id)
|
||||||
|
async def send_state(state):
|
||||||
|
if ws:
|
||||||
|
try:
|
||||||
|
await ws.send_json({
|
||||||
|
"type": "state",
|
||||||
|
"state": serialize_state(state, human_id),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
most_expensive_in_hand = max((c.cost for c in player.hand), default=0)
|
||||||
|
if player.energy < most_expensive_in_hand:
|
||||||
|
for slot in range(BOARD_SIZE):
|
||||||
|
slot_card = player.board[slot]
|
||||||
|
if slot_card is not None and player.energy + slot_card.cost <= most_expensive_in_hand:
|
||||||
|
if ws:
|
||||||
|
try:
|
||||||
|
await ws.send_json({
|
||||||
|
"type": "sacrifice_animation",
|
||||||
|
"instance_id": slot_card.instance_id,
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(0.65)
|
||||||
|
action_sacrifice(state, slot)
|
||||||
|
await send_state(state)
|
||||||
|
await asyncio.sleep(0.35)
|
||||||
|
|
||||||
|
play_order = list(range(BOARD_SIZE))
|
||||||
|
random.shuffle(play_order)
|
||||||
|
for slot in play_order:
|
||||||
|
if player.board[slot] is not None:
|
||||||
|
continue
|
||||||
|
affordable = [i for i, c in enumerate(player.hand) if c.cost <= player.energy]
|
||||||
|
if not affordable:
|
||||||
|
break
|
||||||
|
best = max(affordable, key=lambda i: player.hand[i].cost)
|
||||||
|
action_play_card(state, best, slot)
|
||||||
|
await send_state(state)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
action_end_turn(state)
|
||||||
|
await send_state(state)
|
||||||
|
|
||||||
|
if state.result:
|
||||||
|
from database import SessionLocal
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
record_game_result(state, db)
|
||||||
|
if ws:
|
||||||
|
await ws.send_json({
|
||||||
|
"type": "state",
|
||||||
|
"state": serialize_state(state, human_id),
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
active_deck_ids.pop(human_id, None)
|
||||||
|
active_deck_ids.pop(AI_USER_ID, None)
|
||||||
|
active_games.pop(game_id, None)
|
||||||
|
connections.pop(game_id, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
if state.active_player_id == AI_USER_ID:
|
||||||
|
asyncio.create_task(run_ai_turn(game_id))
|
||||||
@@ -489,15 +489,12 @@ async def _get_card_async(client: httpx.AsyncClient, page_title: str|None = None
|
|||||||
card_type_task, wikirank_task, pageviews_task
|
card_type_task, wikirank_task, pageviews_task
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
(card_type == CardType.other and instance == "") or
|
|
||||||
language_count == 0 or
|
language_count == 0 or
|
||||||
score is None or
|
score is None or
|
||||||
views is None
|
views is None
|
||||||
):
|
):
|
||||||
error_message = f"Could not generate card '{title}': "
|
error_message = f"Could not generate card '{title}': "
|
||||||
if card_type == CardType.other and instance == "":
|
if language_count == 0:
|
||||||
error_message += "Not instance of a class"
|
|
||||||
elif language_count == 0:
|
|
||||||
error_message += "No language pages found"
|
error_message += "No language pages found"
|
||||||
elif score is None:
|
elif score is None:
|
||||||
error_message += "No wikirank score"
|
error_message += "No wikirank score"
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ class GameState:
|
|||||||
result: Optional[GameResult] = None
|
result: Optional[GameResult] = None
|
||||||
last_combat_events: list[CombatEvent] = field(default_factory=list)
|
last_combat_events: list[CombatEvent] = field(default_factory=list)
|
||||||
turn_started_at: Optional[datetime] = None
|
turn_started_at: Optional[datetime] = None
|
||||||
|
ai_difficulty: int = 5 # 1-10, only used for AI games
|
||||||
|
ai_personality: Optional[str] = None # AI personality type, only used for AI games
|
||||||
|
|
||||||
def opponent_id(self, player_id: str) -> str:
|
def opponent_id(self, player_id: str) -> str:
|
||||||
return next(p for p in self.player_order if p != player_id)
|
return next(p for p in self.player_order if p != player_id)
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ from game import (
|
|||||||
)
|
)
|
||||||
from models import Card as CardModel, Deck as DeckModel, DeckCard as DeckCardModel, User as UserModel
|
from models import Card as CardModel, Deck as DeckModel, DeckCard as DeckCardModel, User as UserModel
|
||||||
from card import compute_deck_type
|
from card import compute_deck_type
|
||||||
|
from ai import AI_USER_ID, run_ai_turn, get_random_personality, choose_cards
|
||||||
|
|
||||||
logger = logging.getLogger("app")
|
logger = logging.getLogger("app")
|
||||||
|
|
||||||
AI_USER_ID = "ai"
|
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
active_games: dict[str, GameState] = {}
|
active_games: dict[str, GameState] = {}
|
||||||
@@ -376,15 +375,22 @@ def create_solo_game(
|
|||||||
player_cards: list,
|
player_cards: list,
|
||||||
ai_cards: list,
|
ai_cards: list,
|
||||||
deck_id: str,
|
deck_id: str,
|
||||||
|
difficulty: int = 5,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
ai_personality = get_random_personality()
|
||||||
|
ai_deck = choose_cards(ai_cards, difficulty, ai_personality)
|
||||||
|
|
||||||
player_deck_type = compute_deck_type(player_cards) or "Balanced"
|
player_deck_type = compute_deck_type(player_cards) or "Balanced"
|
||||||
ai_deck_type = compute_deck_type(ai_cards) or "Balanced"
|
ai_deck_type = compute_deck_type(ai_deck) or "Balanced"
|
||||||
|
|
||||||
state = create_game(
|
state = create_game(
|
||||||
user_id, username, player_deck_type, player_cards,
|
user_id, username, player_deck_type, player_cards,
|
||||||
AI_USER_ID, "Computer", ai_deck_type, ai_cards,
|
AI_USER_ID, "Computer", ai_deck_type, ai_deck,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
state.ai_difficulty = difficulty
|
||||||
|
state.ai_personality = ai_personality.value
|
||||||
|
|
||||||
active_games[state.game_id] = state
|
active_games[state.game_id] = state
|
||||||
connections[state.game_id] = {}
|
connections[state.game_id] = {}
|
||||||
active_deck_ids[user_id] = deck_id
|
active_deck_ids[user_id] = deck_id
|
||||||
@@ -422,86 +428,3 @@ def calculate_combat_animation_time(events: list[CombatEvent]) -> float:
|
|||||||
total += ANIMATION_DELAYS["post_combat_buffer"]
|
total += ANIMATION_DELAYS["post_combat_buffer"]
|
||||||
return total
|
return total
|
||||||
|
|
||||||
async def run_ai_turn(game_id: str):
|
|
||||||
state = active_games.get(game_id)
|
|
||||||
if not state or state.result:
|
|
||||||
return
|
|
||||||
if state.active_player_id != AI_USER_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
human_id = state.opponent_id(AI_USER_ID)
|
|
||||||
waited = 0
|
|
||||||
while not connections[game_id].get(human_id) and waited < 10:
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
waited += 0.5
|
|
||||||
|
|
||||||
await asyncio.sleep(calculate_combat_animation_time(state.last_combat_events))
|
|
||||||
|
|
||||||
player = state.players[AI_USER_ID]
|
|
||||||
|
|
||||||
|
|
||||||
ws = connections[game_id].get(human_id)
|
|
||||||
async def send_state(state: GameState):
|
|
||||||
if ws:
|
|
||||||
try:
|
|
||||||
await ws.send_json({
|
|
||||||
"type": "state",
|
|
||||||
"state": serialize_state(state, human_id),
|
|
||||||
})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
most_expensive_in_hand = max((c.cost for c in player.hand), default=0)
|
|
||||||
if player.energy < most_expensive_in_hand:
|
|
||||||
for slot in range(BOARD_SIZE):
|
|
||||||
slot_card = player.board[slot]
|
|
||||||
if slot_card is not None and player.energy + slot_card.cost <= most_expensive_in_hand:
|
|
||||||
if ws:
|
|
||||||
try:
|
|
||||||
await ws.send_json({
|
|
||||||
"type": "sacrifice_animation",
|
|
||||||
"instance_id": slot_card.instance_id,
|
|
||||||
})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
await asyncio.sleep(0.65)
|
|
||||||
action_sacrifice(state, slot)
|
|
||||||
await send_state(state)
|
|
||||||
await asyncio.sleep(0.35)
|
|
||||||
|
|
||||||
play_order = list(range(BOARD_SIZE))
|
|
||||||
random.shuffle(play_order)
|
|
||||||
for slot in play_order:
|
|
||||||
if player.board[slot] is not None:
|
|
||||||
continue
|
|
||||||
affordable = [i for i, c in enumerate(player.hand) if c.cost <= player.energy]
|
|
||||||
if not affordable:
|
|
||||||
break
|
|
||||||
best = max(affordable, key=lambda i: player.hand[i].cost)
|
|
||||||
action_play_card(state, best, slot)
|
|
||||||
await send_state(state)
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
action_end_turn(state)
|
|
||||||
await send_state(state)
|
|
||||||
|
|
||||||
if state.result:
|
|
||||||
from database import SessionLocal
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
record_game_result(state, db)
|
|
||||||
if ws:
|
|
||||||
await ws.send_json({
|
|
||||||
"type": "state",
|
|
||||||
"state": serialize_state(state, human_id),
|
|
||||||
})
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
active_deck_ids.pop(human_id, None)
|
|
||||||
active_deck_ids.pop(AI_USER_ID, None)
|
|
||||||
active_games.pop(game_id, None)
|
|
||||||
connections.pop(game_id, None)
|
|
||||||
return
|
|
||||||
|
|
||||||
if state.active_player_id == AI_USER_ID:
|
|
||||||
asyncio.create_task(run_ai_turn(game_id))
|
|
||||||
|
|||||||
@@ -430,7 +430,10 @@ async def claim_timeout_win(game_id: str, user: UserModel = Depends(get_current_
|
|||||||
return {"message": "Win claimed"}
|
return {"message": "Win claimed"}
|
||||||
|
|
||||||
@app.post("/game/solo")
|
@app.post("/game/solo")
|
||||||
async def start_solo_game(deck_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
|
async def start_solo_game(deck_id: str, difficulty: int = 5, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
|
if difficulty < 1 or difficulty > 10:
|
||||||
|
raise HTTPException(status_code=400, detail="Difficulty must be between 1 and 10")
|
||||||
|
|
||||||
deck = db.query(DeckModel).filter(
|
deck = db.query(DeckModel).filter(
|
||||||
DeckModel.id == uuid.UUID(deck_id),
|
DeckModel.id == uuid.UUID(deck_id),
|
||||||
DeckModel.user_id == user.id
|
DeckModel.user_id == user.id
|
||||||
@@ -448,7 +451,7 @@ async def start_solo_game(deck_id: str, user: UserModel = Depends(get_current_us
|
|||||||
|
|
||||||
ai_cards = db.query(CardModel).filter(
|
ai_cards = db.query(CardModel).filter(
|
||||||
CardModel.user_id == None
|
CardModel.user_id == None
|
||||||
).order_by(func.random()).limit(20).all()
|
).order_by(func.random()).limit(100).all()
|
||||||
|
|
||||||
if len(ai_cards) < 20:
|
if len(ai_cards) < 20:
|
||||||
raise HTTPException(status_code=503, detail="Not enough cards in pool for AI deck")
|
raise HTTPException(status_code=503, detail="Not enough cards in pool for AI deck")
|
||||||
@@ -458,7 +461,7 @@ async def start_solo_game(deck_id: str, user: UserModel = Depends(get_current_us
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
game_id = create_solo_game(str(user.id), user.username, player_cards, ai_cards, deck_id)
|
game_id = create_solo_game(str(user.id), user.username, player_cards, ai_cards, deck_id, difficulty)
|
||||||
|
|
||||||
asyncio.create_task(fill_card_pool())
|
asyncio.create_task(fill_card_pool())
|
||||||
|
|
||||||
@@ -528,3 +531,33 @@ def refresh(req: RefreshRequest, db: Session = Depends(get_db)):
|
|||||||
"refresh_token": create_refresh_token(str(user.id)),
|
"refresh_token": create_refresh_token(str(user.id)),
|
||||||
"token_type": "bearer",
|
"token_type": "bearer",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from ai import AIPersonality, choose_cards
|
||||||
|
from card import generate_cards, Card
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
all_cards: list[Card] = []
|
||||||
|
for i in range(30):
|
||||||
|
print(i)
|
||||||
|
all_cards += generate_cards(10)
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
all_cards.sort(key=lambda x: x.cost, reverse=True)
|
||||||
|
|
||||||
|
print(len(all_cards))
|
||||||
|
def write_cards(cards: list[Card], file: str):
|
||||||
|
with open(file, "w") as fp:
|
||||||
|
fp.write('\n'.join([
|
||||||
|
f"{c.name} - {c.attack}/{c.defense} - {c.cost}"
|
||||||
|
for c in cards
|
||||||
|
]))
|
||||||
|
|
||||||
|
write_cards(all_cards, "output/all.txt")
|
||||||
|
|
||||||
|
for personality in AIPersonality:
|
||||||
|
print(personality.value)
|
||||||
|
for difficulty in range(1,11):
|
||||||
|
chosen_cards = choose_cards(all_cards, difficulty, personality)
|
||||||
|
chosen_cards.sort(key=lambda x: x.cost, reverse=True)
|
||||||
|
write_cards(chosen_cards, f"output/{personality.value}-{difficulty}.txt")
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 162 KiB |
@@ -43,10 +43,10 @@
|
|||||||
result = result.slice().sort((a, b) => {
|
result = result.slice().sort((a, b) => {
|
||||||
let cmp = 0;
|
let cmp = 0;
|
||||||
if (sortBy === 'name') cmp = a.name.localeCompare(b.name);
|
if (sortBy === 'name') cmp = a.name.localeCompare(b.name);
|
||||||
else if (sortBy === 'cost') cmp = a.cost - b.cost || a.name.localeCompare(b.name);
|
else if (sortBy === 'cost') cmp = b.cost - a.cost || a.name.localeCompare(b.name);
|
||||||
else if (sortBy === 'attack') cmp = a.attack - b.attack || a.name.localeCompare(b.name);
|
else if (sortBy === 'attack') cmp = b.attack - a.attack || a.name.localeCompare(b.name);
|
||||||
else if (sortBy === 'defense') cmp = a.defense - b.defense || a.name.localeCompare(b.name);
|
else if (sortBy === 'defense') cmp = b.defense - a.defense || a.name.localeCompare(b.name);
|
||||||
else if (sortBy === 'rarity') cmp = RARITY_ORDER[a.card_rarity] - RARITY_ORDER[b.card_rarity] || a.name.localeCompare(b.name);
|
else if (sortBy === 'rarity') cmp = RARITY_ORDER[b.card_rarity] - RARITY_ORDER[a.card_rarity] || a.name.localeCompare(b.name);
|
||||||
return sortAsc ? cmp : -cmp;
|
return sortAsc ? cmp : -cmp;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
if (!selectedDeckId || selectedDeck?.card_count < 20) return;
|
if (!selectedDeckId || selectedDeck?.card_count < 20) return;
|
||||||
error = '';
|
error = '';
|
||||||
phase = 'queuing';
|
phase = 'queuing';
|
||||||
const res = await apiFetch(`${API_URL}/game/solo?deck_id=${selectedDeckId}`, {
|
const res = await apiFetch(`${API_URL}/game/solo?deck_id=${selectedDeckId}&difficulty=5`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user