Skip to content
Open
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
34 changes: 34 additions & 0 deletions Buttons/DiscoveryTile.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import json
import random as _random
import discord
from Buttons.Upgrade import UpgradeButtons
from helpers.GamestateHelper import GamestateHelper
Expand All @@ -8,7 +9,35 @@
from discord.ui import View, Button


# Discovery tile IDs that cannot be placed in a Nebula Subsector.
# Per the Galactic Events rulebook (p. 4): "the Ancient Orbital and Ancient
# Monolith cannot be placed in Nebula Sectors; if they are revealed in a
# Nebula Sector, draw a new Discovery Tile before choosing whether to keep
# it for its benefit or for VP."
_NEBULA_FORBIDDEN_DISCS = {"orb", "mon"}


class DiscoveryTileButtons:
@staticmethod
def _maybe_reroll_for_nebula(game: GamestateHelper, tile: str, disc: str) -> str:
"""If ``tile`` is a nebula subsector and ``disc`` is one of the
forbidden Ancient Orbital / Ancient Monolith tiles, shuffle it back
into the deck and pop a replacement. Returns the (possibly new) disc
id. Loops until a non-forbidden disc is drawn or the deck would
empty; falls back to the last drawn disc if the deck is exhausted.
"""
from helpers import NebulaHelper as NH
if not NH.is_nebula_subsector(tile):
return disc
attempts = 0
deck = game.gamestate.get("discTiles") or []
while disc in _NEBULA_FORBIDDEN_DISCS and deck and attempts < 32:
deck.insert(_random.randint(0, len(deck)), disc)
disc = deck.pop()
attempts += 1
game.update()
return disc

@staticmethod
async def exploreDiscoveryTile(game: GamestateHelper, tile: str, interaction: discord.Interaction, player):
# if "discTiles" not in game.gamestate:
Expand All @@ -17,6 +46,11 @@ async def exploreDiscoveryTile(game: GamestateHelper, tile: str, interaction: di
await interaction.followup.send("No discovery tile in tile " + tile)
return
disc = game.getNextDiscTile(tile)

# Nebula reroll: Ancient Orbital and Ancient Monolith cannot be placed
# in a Nebula Subsector. See Galactic Events rulebook p. 4.
disc = DiscoveryTileButtons._maybe_reroll_for_nebula(game, tile, disc)

with open("data/discoverytiles.json") as f:
discTile_data = json.load(f)
discName = discTile_data[disc]["name"]
Expand Down
41 changes: 32 additions & 9 deletions Buttons/Explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,24 @@ def getTilesToExplore(game: GamestateHelper, player):
else:
with open("data/tileAdjacencies.properties", "rb") as f:
configs.load(f)
from helpers import NebulaHelper as NH
tilesViewed = []
playerTiles = ExploreButtons.getListOfTilesPlayerIsIn(game, player)
for tile in playerTiles:
for index, adjTile in enumerate(configs.get(tile)[0].split(",")):
tile_orientation_index = (index + int(game.gamestate["board"][tile]["orientation"]) // 60) % 6
tile_record = game.gamestate["board"][tile]
# Nebula parents have no wormholes / no ships — they shouldn't
# appear here, but be defensive.
if NH.is_nebula_parent(tile_record):
continue
# Resolve the parent for adjacency lookups when the player's
# presence tile is a nebula subsector.
lookup_tile = NH.get_parent_position(tile) if NH.is_nebula_subsector(tile) else tile
if lookup_tile not in configs:
continue
for index, adjTile in enumerate(configs.get(lookup_tile)[0].split(",")):
tile_orientation_index = (index + int(tile_record["orientation"]) // 60) % 6
if all([adjTile not in tilesViewed,
tile_orientation_index in game.gamestate["board"][tile]["wormholes"],
tile_orientation_index in tile_record.get("wormholes", []),
"back" in game.gamestate.get("board", {}).get(str(adjTile), {}).get("sector", [])]):
if int(adjTile) > 299 and len(game.gamestate["tile_deck_300"]) == 0:
continue
Expand Down Expand Up @@ -163,9 +174,21 @@ async def placeTile(game: GamestateHelper, interaction: discord.Interaction, pla
view2 = View()
game.update_player(player_helper)
await interaction.channel.send(f"Tile added to position {msg[1]}")
if game.gamestate["board"][msg[1]]["ancient"] == 0 or player["name"] == "Descendants of Draco":
placed_tile = game.gamestate["board"][msg[1]]
# Nebula sectors: subsector ancients/discoveries are resolved
# independently (Ancient is pre-spawned in subsector B; discoveries
# in A and C are claimed at end of Combat Phase). The hex itself can
# never carry an influence disc, so skip both prompts.
if placed_tile.get("type") == "nebula" and placed_tile.get("is_nebula_parent"):
await interaction.channel.send(
f"{player['player_name']} this is a Nebula sector. An Ancient occupies one "
"subsector and discovery tiles wait in the other two — they will be "
"claimed at the end of the Combat Phase by any ship still present in those subsectors. "
"Influence discs cannot be placed in a Nebula sector."
)
elif placed_tile["ancient"] == 0 or player["name"] == "Descendants of Draco":
view = View()
if "bh" in game.gamestate["board"][msg[1]].get("type", ""):
if "bh" in placed_tile.get("type", ""):
await interaction.channel.send("This is a black hole tile, "
"so its discovery tile cannot be claimed until a ship moves in, "
"at which point a die will be rolled and it might teleport.")
Expand All @@ -177,12 +200,12 @@ async def placeTile(game: GamestateHelper, interaction: discord.Interaction, pla
await interaction.channel.send(f"{player['player_name']}, choose whether or not"
" to place influence in the tile." + game.displayPlayerStats(player),
view=view)
if all([game.gamestate["board"][msg[1]]["ancient"] == 0,
game.gamestate["board"][msg[1]]["disctile"] > 0]):
if all([placed_tile["ancient"] == 0,
placed_tile["disctile"] > 0]):
await DiscoveryTileButtons.exploreDiscoveryTile(game, msg[1], interaction, player)

else:
ancients = game.gamestate["board"][msg[1]]["ancient"]
ancients = placed_tile["ancient"]
await interaction.channel.send(f"There are {ancients} ancients in this tile, "
"so you will not be able to claim it until you fight and destroy them.")

Expand Down
49 changes: 39 additions & 10 deletions Buttons/Influence.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,35 @@ class InfluenceButtons:

@staticmethod
def areTwoTilesAdjacent(game: GamestateHelper, tile1, tile2, configs, wormholeGen: bool):
from helpers import NebulaHelper as NH
board = game.gamestate["board"]

# Two subsectors of the same nebula are always adjacent via their
# internal wormhole connections (A<->B<->C, all three pairs).
if NH.is_nebula_subsector(tile1) and NH.is_nebula_subsector(tile2):
if NH.get_parent_position(tile1) == NH.get_parent_position(tile2):
return tile1 in board and tile2 in board

def lookup_position(t):
# When walking the parent's adjacency table, route subsector
# positions back to their parent hex for lookups (the .properties
# files don't know about subsectors).
return NH.get_parent_position(t) if NH.is_nebula_subsector(t) else t

def is_adjacent(tile_a, tile_b):
for index, adjTile in enumerate(configs.get(tile_a)[0].split(",")):
if tile_a in game.gamestate["board"] and tile_b in game.gamestate["board"] and adjTile == tile_b:
tile_orientation_index = (index + (int(game.gamestate["board"][tile_a]["orientation"]) // 60)) % 6
if tile_orientation_index in game.gamestate["board"][tile_a].get("wormholes", []):
lookup_a = lookup_position(tile_a)
lookup_b = lookup_position(tile_b)
if lookup_a not in configs:
return False
for index, adjTile in enumerate(configs.get(lookup_a)[0].split(",")):
if (tile_a in board and tile_b in board
and adjTile == lookup_b):
tile_orientation_index = (index + (int(board[tile_a].get("orientation", 0)) // 60)) % 6
if tile_orientation_index in board[tile_a].get("wormholes", []):
return True
if tile_a in game.gamestate["board"] and tile_b in game.gamestate["board"]:
if game.gamestate["board"][tile_a].get("warp", 0) == 1:
if game.gamestate["board"][tile_b].get("warp", 0) == 1:
if tile_a in board and tile_b in board:
if board[tile_a].get("warp", 0) == 1:
if board[tile_b].get("warp", 0) == 1:
return True
return False

Expand Down Expand Up @@ -58,8 +78,9 @@ def getTilesToInfluence(game: GamestateHelper, player):
wormHoleGen = True
tilesToInfluence = []
playerTiles = InfluenceButtons.getListOfTilesPlayerIsInForInfluence(game, player)
from helpers import NebulaHelper as NH
for tile in playerTiles:
for adjTile in configs.get(tile)[0].split(","):
for adjTile in NH.adjacent_positions_from_configs(game, tile, configs):
if adjTile not in tilesViewed and InfluenceButtons.areTwoTilesAdjacent(game, tile, adjTile,
configs, wormHoleGen):
tilesViewed.append(adjTile)
Expand All @@ -69,12 +90,16 @@ def getTilesToInfluence(game: GamestateHelper, player):
continue
if "exploded" in game.gamestate["board"][adjTile].get("type", ""):
continue
# Nebula sectors (parent or subsector) cannot ever hold an
# influence disc per the Galactic Events rules.
if game.gamestate["board"][adjTile].get("type", "") == "nebula":
continue
if "player_ships" not in game.gamestate["board"][adjTile]:
continue
playerShips = game.gamestate["board"][adjTile]["player_ships"][:]
playerShips.append(player["color"])
if all([game.gamestate["board"][adjTile].get("owner") == 0,
ExploreButtons.doesPlayerHaveUnpinnedShips(player, playerShips, game, adjTile),
ExploreButtons.doesPlayerHaveUnpinnedShips(player, playerShips, game, adjTile),
len(Combat.findPlayersInTile(game, adjTile)) < 2, "ai-grd" not in playerShips]):
tilesToInfluence.append(adjTile)
if tile_map[tile].get("warp", 0) == 1:
Expand All @@ -87,16 +112,20 @@ def getTilesToInfluence(game: GamestateHelper, player):
continue
if "exploded" in game.gamestate["board"][tile2].get("type", ""):
continue
if game.gamestate["board"][tile2].get("type", "") == "nebula":
continue
if "player_ships" not in game.gamestate["board"][tile2]:
continue
playerShips = game.gamestate["board"][tile2]["player_ships"][:]
playerShips.append(player["color"])
if all([game.gamestate["board"][tile2].get("owner") == 0,
ExploreButtons.doesPlayerHaveUnpinnedShips(player, playerShips, game, tile2),
ExploreButtons.doesPlayerHaveUnpinnedShips(player, playerShips, game, tile2),
len(Combat.findPlayersInTile(game, tile2)) < 2]):
tilesToInfluence.append(tile2)
if tile not in tilesViewed:
tilesViewed.append(tile)
if game.gamestate["board"][tile].get("type", "") == "nebula":
continue
playerShips = game.gamestate["board"][tile]["player_ships"][:]
playerShips.append(player["color"])
if all([game.gamestate["board"][tile].get("owner") == 0,
Expand Down
9 changes: 8 additions & 1 deletion Buttons/Move.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,27 @@ def getTilesInRange(game: GamestateHelper, player, origin: str, shipRange: int,
if jumpDrivePresent == 1:
jumpDrive = True

from helpers import NebulaHelper as NH

def recursive_search(pos, distance, visited, jumpDriveAvailable):
if distance > shipRange:
return
if pos not in tile_map:
return
if "player_ships" not in tile_map[pos]:
return
# Nebula parent records are pure render anchors — ships only ever
# live in the subsectors. Don't treat the parent hex as a movable
# destination.
if NH.is_nebula_parent(tile_map[pos]):
return
visited.add(pos)
player_ships = tile_map[pos]["player_ships"][:]
player_ships.append(f"{player['color']}-cruiser") # adding phantom ship so I can reuse a method
if not ExploreButtons.doesPlayerHaveUnpinnedShips(player, player_ships, game, pos):
return

for adjTile in configs.get(pos)[0].split(","):
for adjTile in NH.adjacent_positions_from_configs(game, pos, configs):
if adjTile in tile_map and InfluenceButtons.areTwoTilesAdjacent(game, pos, adjTile,
configs, wormHoleGen):
recursive_search(adjTile, distance + 1, visited, jumpDriveAvailable)
Expand Down
4 changes: 4 additions & 0 deletions Buttons/Turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ async def runUpkeep(game: GamestateHelper, interaction: discord.Interaction):
await interaction.channel.send("It appears some tiles are still in conflict. "
"Please resolve them before running upkeep")
return
# End-of-Combat-Phase nebula discovery resolution. A discovery tile in
# a nebula subsector is claimed only if the player has a ship in that
# subsector at this point.
await Combat.claimNebulaDiscoveries(game, interaction)
if "peopleToCheckWith" in game.gamestate and len(game.gamestate["peopleToCheckWith"]) > 0:
msg = " Still waiting on the following players to hit the ready for upkeep button:\n"
for color2 in game.gamestate["peopleToCheckWith"]:
Expand Down
7 changes: 5 additions & 2 deletions commands/setup_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ async def create_new_game(self, interaction: discord.Interaction, game_name: str
galactic_event_tiles: Optional[bool] = False,
hyperlanes: Optional[bool] = False,
community_parts: Optional[bool] = False,
ban_factions: Optional[bool] = False):
ban_factions: Optional[bool] = False,
enable_nebulas: Optional[bool] = True):
"""
:param ai_ship_type: Choose which type of AI ships to use.
:param rift_cannon: Rift cannons are enabled by default.
Expand All @@ -162,6 +163,7 @@ async def create_new_game(self, interaction: discord.Interaction, game_name: str
:param hyperlanes: Hyperlanes for 4p and 5p are default off.
:param ban_factions: Used to ban 10-playerCount factions from the draft.
:param community_parts: Turns on commnity changes to improved hull and phase shield.
:param enable_nebulas: Galactic Events nebula sectors split into 3 subsectors. On by default.
:return:
"""
temp_player_list = [player1, player2, player3, player4, player5, player6,player7,player8,player9]
Expand All @@ -174,7 +176,8 @@ async def create_new_game(self, interaction: discord.Interaction, game_name: str
ai_ships = "def"
else:
ai_ships = ai_ship_type.value
new_game = GameInit(game_name, player_list, ai_ships, rift_cannon, turn_order_variant, community_parts)
new_game = GameInit(game_name, player_list, ai_ships, rift_cannon, turn_order_variant,
community_parts, enable_nebulas)
new_game.create_game()

async def get_or_create_category(guild: discord.Guild, category_name):
Expand Down
Loading