Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
__pycache__
venv/
.venv/
.python-version
12 changes: 12 additions & 0 deletions src/game/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from . import gamelogic
from . import factions
from . import strategy_cards
from ..typing import *

from discord.ext import commands
Expand All @@ -22,8 +23,10 @@ def __init__(
) -> None:
"""Initialize the Commands cog with factions."""
self.factions = factions.read_factions()
self.strategy_cards = strategy_cards.read_strategy_cards()
self.logic = gamelogic.GameLogic(bot, engine)


async def __send_embed_or_pretty_err(self, ctx: commands.Context, result: Result[discord.Embed]) -> None:
match result:
case Ok(embed):
Expand All @@ -44,6 +47,15 @@ def __game_id(self, ctx: commands.Context):
# Let's use the channel ID for the game ID.
return ctx.channel.id

@commands.command(name="strategy-cards")
async def strat_cards(
self, ctx: commands.Context) -> None:
"""Returns the list of strategy cards in the game."""

await ctx.send(
f"{'\n'.join([str(sc) for sc in self.strategy_cards])}"
)

@commands.command(name="factions")
async def random_factions(
self, ctx: commands.Context, number: int = 8, *, sources: str = ""
Expand Down
9 changes: 9 additions & 0 deletions src/game/data/ti4_strategy_cards.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Initiative order,Strategy card,Primary ability,Secondary ability
1,Leadership,Gain 3 command tokens. Spend any amount of influence to gain 1 command token for every 3 influence spent.,Spend any amount of influence to gain 1 command token for every 3 influence spent.
2,Diplomacy,Choose 1 system other than the Mecatol Rex system that contains a planet you control; each other player places a command token from their reinforcements in the chosen system. Then ready up to 2 exhausted planets you control.,Spend 1 token from your strategy pool to ready up to 2 exhausted planets you control.
3,Politics,Choose a player other than the speaker. That player gains the speaker token. Draw 2 action cards. Look at the top 2 cards of the agenda deck. Place each card on the top or bottom of the deck in any order.,Spend 1 token from your strategy pool to draw 2 action cards.
4,Construction,Place 1 PDS or 1 Space Dock on a planet you control. Place 1 PDS on a planet you control.,Spend 1 token from your strategy pool and place it in any system; you may place either 1 space dock or 1 PDS on a planet you control in that system.
5,Trade,Gain 3 trade goods. Replenish commodities. Choose any number of other players. Those players use the secondary ability of this strategy card without spending a command token.,Spend 1 token from your strategy pool to replenish your commodities.
6,Warfare,Remove 1 of your command tokens from the game board; then gain 1 command token. Redistribute any number of the command tokens on your command sheet, Spend 1 token from your strategy pool to use the Production ability of 1 of your space docks in your home system (This token is not placed in your home system).
7,Technology,Research 1 technology. Spend 6 resources to research 1 technology.,Spend 1 token from your strategy pool and 4 resources to research 1 technology.
8,Imperial,Immediately score 1 public objective if you fulfill its requirements. Gain 1 victory point if you control Mecatol Rex; otherwise draw 1 secret objective.,Spend 1 token from your strategy pool to draw 1 secret objective.
223 changes: 213 additions & 10 deletions src/game/draftingmodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
from . import model, controller
from . import gamelogic
from . import factions as fs
from . import strategy_cards
from ..typing import *

from itertools import batched
from sqlalchemy.orm import Session, attributes
from typing import Optional
from typing import Optional, List, Dict


class GameMode:
Expand Down Expand Up @@ -44,6 +45,8 @@ def create(cls, game: model.Game) -> GameMode:
return PicksOnly(game)
case model.DraftingMode.PICKS_AND_BANS:
return PicksAndBans(game)
case model.DraftingMode.HOMEBREW_DRAFT:
return HomeBrewDraft(game)
return GameMode(game)

def draft(
Expand Down Expand Up @@ -251,7 +254,6 @@ class PicksAndBans(GameMode):
def draft(
self,
session: Session,
game: model.Game,
player: model.GamePlayer,
faction: Optional[str],
) -> Optional[str]:
Expand All @@ -262,13 +264,13 @@ def draft(
if not faction:
return f"Your available factions are:\n{"\n".join(player.factions)}"

current_drafter = self.controller.current_drafter(session, game)
current_drafter = self.controller.current_drafter(session, self.game)

if game.turn != player.turn_order:
if self.game.turn != player.turn_order:
return f"It is not your turn to draft! It is {current_drafter.player.name}'s turn"

all_bans = []
for game_player in game.game_players:
for game_player in self.game.game_players:
if game_player.bans:
all_bans.extend(game_player.bans)

Expand All @@ -285,10 +287,10 @@ def draft(
return f"Faction {best} has already been banned."

player.faction = best
game.turn += 1
self.game.turn += 1

other_players = [
other for other in game.game_players if other.player_id != player.player_id
other for other in self.game.game_players if other.player_id != player.player_id
]
for other_player in other_players:
# Somehow the typing doesn't work without the if-statement
Expand All @@ -297,14 +299,14 @@ def draft(
other_player.factions.remove(player.faction)
session.merge(other_player)

session.merge(game)
session.merge(self.game)
session.merge(player)
lines = [f"{player.player.name} has selected {player.faction}."]
if game.turn == len(game.game_players):
if self.game.turn == len(self.game.game_players):
return None
session.commit()

current_drafter = self.controller.current_drafter(session, game)
current_drafter = self.controller.current_drafter(session, self.game)

lines.append(f"Next drafter is <@{current_drafter.player_id}>. Use !draft.")
return "\n".join(lines)
Expand Down Expand Up @@ -416,3 +418,204 @@ def ban(
session.commit()

return "\n".join(lines)

class HomeBrewDraft(GameMode):
def draft(
self,
session: Session,
player: model.GamePlayer,
draft_choice: Optional[str],
) -> Result[discord.Embed|GameStarted]:

if player.faction and player.position and player.strategy_card:
return Ok(discord.Embed(
title="Drafting phase",
description=f"You have drafted {player.faction}, starting with {player.strategy_card} in position {player.position}",
color=discord.Color.green()
))


strategy_cards_map : Dict[str, strategy_cards.StrategyCard] = {sc.name.lower(): sc for sc in strategy_cards.read_strategy_cards()}
available_strategy_cards: Dict[str, strategy_cards.StrategyCard] = {sc.name.lower(): sc for sc in strategy_cards_map.values() if sc.name not in [other.strategy_card for other in self.game.game_players if other.strategy_card]}
available_positions: List[int] = [x for x in range(1, len(self.game.game_players) + 1) if x not in [other.position for other in self.game.game_players if other.position]]

lines = []
if not draft_choice:
if not player.faction:
lines.append("You may draft **Faction**.")
if not player.strategy_card:
lines.append("You may draft **Strategy card**.")
if not player.position:
lines.append("You may draft **Starting position**.")
if not player.faction:
lines.append(f"\nYour available Factions are:\n* {"\n* ".join(player.factions)}")
if not player.strategy_card:
lines.append(f"\nYour available Strategy cards are:\n{"\n".join([str(sc) for sc in sorted(available_strategy_cards.values(), key=lambda x: x.initiative_order)])}")
if not player.position:
lines.append(f"\nYour available Starting positions are: {", ".join(map(str,available_positions))}")
return Ok(discord.Embed(
title="Draft Options",
description="\n".join(lines),
color=discord.Color.blue()
))

current_drafter = self.controller.current_drafter(session, self.game)

if self.game.turn != player.turn_order:
return Err(f"It is not your turn to draft! It is {current_drafter.player.name}'s turn")


def get_direction(game: model.Game) -> int:
num_drafts = sum([bool(player.faction) + bool(player.strategy_card) + bool(player.position) for player in game.game_players])
return 1 if ((num_drafts // len(game.game_players)) % 2) == 0 else -1

# This is used later, but need to be calculated before the drafting
direction = get_direction(self.game)

all_bans: List[str] = []
for game_player in self.game.game_players:
if game_player.bans:
all_bans.extend(game_player.bans)

available_factions = player.factions.copy()
available_factions.extend(all_bans)

# Draft position
if draft_choice.isdigit():
if not player.position:
if int(draft_choice) not in available_positions:
return Err(f"You can't draft position {draft_choice}.")
else:
player.position = int(draft_choice)
lines.append(f"Player <@{current_drafter.player_id}> drafted position {player.position}")
else:
return Err("You've already drafted position")
# Draft strategy card
elif draft_choice.lower() in strategy_cards_map.keys():
if not player.strategy_card:
if draft_choice.lower() not in available_strategy_cards.keys():
return Err(f"Strategy card {available_strategy_cards[draft_choice.lower()].name} is already picked.")
else:
player.strategy_card = available_strategy_cards[draft_choice.lower()].name
lines.append(f"Player <@{current_drafter.player_id}> drafted Strategy card {player.strategy_card}")
else:
return Err("You've already drafted Strategy card")

# Check what the best match is. If it's a faction, draft faction
else:
possible_matches : Dict[str, Union[str, strategy_cards.StrategyCard]] = {x.lower(): x for x in available_factions}
possible_matches.update({sc.name.lower(): sc for sc in strategy_cards_map.values()})

best = gamelogic.GameLogic._closest_match(draft_choice.lower(), possible_matches.keys())
if not best:
return Err(f"Not possible to match {draft_choice} to anything. Check your spelling or available picks")
if best in strategy_cards_map.keys():
if not player.strategy_card:
return Err(f"Did you mean to draft {strategy_cards_map[best].name}? Strict spelling is enforced for strategy cards.")
else:
return Err(f"Best match was {strategy_cards_map[best].name}, but you've already drafted Strategy card")
if not player.faction:
# Check if this faction is already banned by anyone
value = possible_matches[best]
if value in all_bans:
return Err(f"Faction {value} is banned.")
if isinstance(value, str):
player.faction = value
lines = [f"<@{current_drafter.player_id}> has drafted {player.faction}."]
else:
return Err("An error occurred when matching text to strategy card or faction.")

# Update other players available factions
other_players = [
other for other in self.game.game_players if other.player_id != player.player_id
]
for other_player in other_players:
if player.faction:
other_player.factions.remove(player.faction)
session.merge(other_player)
else:
return Err("You've already drafted faction.")


def check_draft_over(game: model.Game) -> bool:
done = [bool(player.faction) and bool(player.strategy_card) and bool(player.position) for player in game.game_players]
return all(done)

self.game.turn += direction

session.merge(player)
if self.game.turn == -1 or self.game.turn == len(self.game.game_players):
if check_draft_over(self.game):
session.merge(self.game)
session.commit()
return Ok(GameStarted())
else:
# Flip direction and go back.
self.game.turn -= direction
session.merge(self.game)
session.commit()

current_drafter = self.controller.current_drafter(session, self.game)

lines.append(f"Next drafter is <@{current_drafter.player_id}>. Use !draft.")
return Ok(discord.Embed(
title="Next Drafter",
description="\n".join(lines),
color=discord.Color.blue()
))

def start(self, session: Session, factions: fs.Factions) -> Result[discord.Embed]:

self.direction: int = 1

players = self.game.game_players
number_of_players = len(players)

factions_per_player = self.game.game_settings.factions_per_player
fs = factions.get_random_factions(
number_of_players * factions_per_player, self._get_faction_sources()
)
if len(fs) < number_of_players * factions_per_player:
return Err(
f"There are too many factions selected per player. Max allowed for a {number_of_players} player game is {len(fs)//number_of_players}."
)
fs = [faction.name for faction in fs]

turn_order = random.sample(range(number_of_players), number_of_players)
faction_slices = batched(fs, factions_per_player)

factions_lines = []

player_from_turn = {}
for i, (player, player_factions) in enumerate(
zip(self.game.game_players, faction_slices)
):
player.turn_order = turn_order[i]
player_from_turn[player.turn_order] = player.player.name
player.factions = list(player_factions)
factions_lines.extend(
list(map(lambda x: f"{x} ({player.player.name})", player_factions))
)
session.merge(player)

players_info_lines = []
for i in range(number_of_players):
name = player_from_turn[i]
players_info_lines.append(f"{name}")

self.game.game_state = model.GameState.DRAFT
session.merge(self.game)
session.commit()

lines = [
f"State: {self.game.game_state.value}\n\nSnake drafting enabled\n\nPlayers (in draft order):\n{"\n".join(players_info_lines)}\n\nFactions:\n{"\n".join(factions_lines)}"
]

current_drafter = self.controller.current_drafter(session, self.game)

lines.append(f"<@{current_drafter.player_id}> begins drafting. Use !draft.")
return Ok(discord.Embed(
title="🎲 Drafting Phase",
description="\n".join(lines),
color=discord.Color.blue()
))
16 changes: 10 additions & 6 deletions src/game/gamelogic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import enum

from . import factions as fs
from . import strategy_cards
from . import model
from . import draftingmodes
from . import controller
Expand All @@ -18,7 +19,7 @@
from sqlalchemy import inspect, Enum, Boolean, String, Integer, select
from sqlalchemy.orm import Session
from string import Template
from typing import Optional, Dict, Any, Iterable, Sequence
from typing import Optional, Dict, Any, Iterable, Sequence, List

from blinker import signal

Expand Down Expand Up @@ -243,7 +244,10 @@ def cancel(self, game_id: int) -> Result[discord.Embed]:
def _start_game(self, session: Session, game: model.Game) -> discord.Embed:
players_info_lines = []
for player in game.game_players:
players_info_lines.append(f"{player.player.name} playing {player.faction}")
if game.game_settings.drafting_mode == model.DraftingMode.HOMEBREW_DRAFT:
players_info_lines.append(f"<@{player.player_id}> playing {player.faction}, starting with {player.strategy_card} at position {player.position}")
else:
players_info_lines.append(f"{player.player.name} playing {player.faction}")

game.game_state = model.GameState.STARTED
session.merge(game)
Expand Down Expand Up @@ -552,11 +556,11 @@ def get_valid_values(dtype):

dtype = valid_keys[property]
if isinstance(dtype, Enum):
enum_list = list(dtype.enums)
best_value = GameLogic._closest_match(value, enum_list)
enum_map: Dict[str, Any] = {text.lower(): text for text in dtype.enums}
best_value = GameLogic._closest_match(value.lower(), enum_map.keys())
if not best_value:
return Err(f"Valid values are: {enum_list}")
new_value = best_value
return Err(f"Valid values are: {enum_map.values()}")
new_value = enum_map[best_value]
elif isinstance(dtype, Boolean):
val = value.lower()
if val in ["true", "t", "yes", "y", "1"]:
Expand Down
Loading