diff --git a/src/pirats/crud_scene.py b/src/pirats/crud_scene.py index 49d33de..1493c6f 100644 --- a/src/pirats/crud_scene.py +++ b/src/pirats/crud_scene.py @@ -307,9 +307,19 @@ def play_card_on_obstacle( return True, "Card played successfully.", res_dict -def toggle_objective(db: Session, player_id: str, obj_type: str, status: bool): - """Toggles Personal or Crew objectives and adjusts Ranks accordingly.""" - player = get_player(db, player_id) +def toggle_objective(db: Session, game_id: str, target_id: str, obj_type: str, status: bool): + """Toggles Personal or Crew objectives and adjusts states/Ranks accordingly.""" + if obj_type.startswith("crew_"): + game = get_game(db, game_id) + if not game: return + if obj_type == "crew_1": game.completed_crew_1 = status + elif obj_type == "crew_2": game.completed_crew_2 = status + elif obj_type == "crew_3": game.completed_crew_3 = status + db.add(game) + db.commit() + return + + player = get_player(db, target_id) if not player: return @@ -324,14 +334,56 @@ def toggle_objective(db: Session, player_id: str, obj_type: str, status: bool): elif obj_type == "personal_2": if status and not player.completed_personal_2: rank_diff = 1 + player.needs_name = True elif not status and player.completed_personal_2: rank_diff = -1 + player.needs_name = False player.completed_personal_2 = status elif obj_type == "personal_3": + if status and not player.completed_personal_3: + player.needs_rank_3_bonus = True + player.is_dead = True + elif not status and player.completed_personal_3: + player.needs_rank_3_bonus = False + player.is_dead = False player.completed_personal_3 = status player.rank += rank_diff player.rank = max(1, player.rank) db.add(player) db.commit() + +def rollback_card_play(db: Session, obstacle_id: str) -> Tuple[bool, str]: + obstacle = db.get(Obstacle, obstacle_id) + if not obstacle: + return False, "Obstacle not found." + + played_list = json.loads(obstacle.played_cards) + if not played_list: + return False, "No cards to roll back." + + last_play = played_list.pop() + card_code = last_play["card"] + player_id = last_play["player_id"] + + # Update obstacle's value + if played_list: + prev_card_code = played_list[-1]["card"] + obstacle.current_value = cards.parse_card(prev_card_code)["numeric_value"] + else: + obstacle.current_value = cards.get_obstacle_info(obstacle.original_card)["initial_value"] + + obstacle.played_cards = json.dumps(played_list) + db.add(obstacle) + + # Return card to player's hand + player = get_player(db, player_id) + if player: + hand = get_player_hand(player) + hand.append(card_code) + set_player_hand(player, hand) + db.add(player) + + db.commit() + return True, "Rolled back the last played card." diff --git a/src/pirats/database.py b/src/pirats/database.py index 5e42602..ba9732e 100644 --- a/src/pirats/database.py +++ b/src/pirats/database.py @@ -18,9 +18,26 @@ def create_db_and_tables(): with Session(engine) as session: try: session.execute(text("ALTER TABLE game ADD COLUMN crew_name VARCHAR")) - session.commit() except Exception: pass # Column already exists or table doesn't exist yet + + columns = [ + ("game", "completed_crew_1", "BOOLEAN DEFAULT FALSE"), + ("game", "completed_crew_2", "BOOLEAN DEFAULT FALSE"), + ("game", "completed_crew_3", "BOOLEAN DEFAULT FALSE"), + ("player", "needs_name", "BOOLEAN DEFAULT FALSE"), + ("player", "needs_rank_3_bonus", "BOOLEAN DEFAULT FALSE"), + ("player", "is_dead", "BOOLEAN DEFAULT FALSE"), + ("player", "is_ghost", "BOOLEAN DEFAULT FALSE"), + ] + + for table, col, def_type in columns: + try: + session.execute(text(f"ALTER TABLE {table} ADD COLUMN {col} {def_type}")) + except Exception: + pass + + session.commit() def get_session(): with Session(engine) as session: diff --git a/src/pirats/models.py b/src/pirats/models.py index 6732c65..3fccc47 100644 --- a/src/pirats/models.py +++ b/src/pirats/models.py @@ -10,6 +10,9 @@ class Game(SQLModel, table=True): current_scene_number: int = Field(default=1) extra_obstacles: int = Field(default=0) # Extra obstacles for next scene (from Jokers) deck_cards: str = Field(default="[]") # JSON string of list of card codes, e.g. ["AS", "10D", "KH", ...] + completed_crew_1: bool = Field(default=False) # Steal a Ship + completed_crew_2: bool = Field(default=False) # Choose a Captain + completed_crew_3: bool = Field(default=False) # Commit Piracy players: List["Player"] = Relationship(back_populates="game", cascade_delete=True) obstacles: List["Obstacle"] = Relationship(back_populates="game", cascade_delete=True) @@ -50,6 +53,12 @@ class Player(SQLModel, table=True): completed_personal_2: bool = Field(default=False) # Earn a Name completed_personal_3: bool = Field(default=False) # Die like a Pirate + # States for Milestones & Death + needs_name: bool = Field(default=False) + needs_rank_3_bonus: bool = Field(default=False) + is_dead: bool = Field(default=False) + is_ghost: bool = Field(default=False) + # Relationships game: Optional[Game] = Relationship(back_populates="players") diff --git a/src/pirats/routes_scene.py b/src/pirats/routes_scene.py index 97c7385..5736154 100644 --- a/src/pirats/routes_scene.py +++ b/src/pirats/routes_scene.py @@ -111,22 +111,86 @@ def play_joker_route( def toggle_objective_route( game_id: str, player_id: str, - type: str, # "personal_1", "personal_2", "personal_3" + type: str, # "personal_1", "personal_2", "personal_3", or "crew_1", etc. + db: Session = Depends(get_session) +): + if type.startswith("crew_"): + game = crud.get_game(db, game_id) + if not game: raise HTTPException(status_code=404) + current_status = False + if type == "crew_1": current_status = game.completed_crew_1 + elif type == "crew_2": current_status = game.completed_crew_2 + elif type == "crew_3": current_status = game.completed_crew_3 + crud.toggle_objective(db, game_id, player_id, type, not current_status) + else: + player = crud.get_player(db, player_id) + if not player: raise HTTPException(status_code=404, detail="Player not found") + current_status = False + if type == "personal_1": current_status = player.completed_personal_1 + elif type == "personal_2": current_status = player.completed_personal_2 + elif type == "personal_3": current_status = player.completed_personal_3 + crud.toggle_objective(db, game_id, player_id, type, not current_status) + + return HTMLResponse(status_code=204) + +# Deep rollbacks a card play +@router.post("/game/{game_id}/scene/rollback") +def rollback_card_play_route( + game_id: str, + obstacle_id: str = Form(...), + db: Session = Depends(get_session) +): + ok, msg = crud.rollback_card_play(db, obstacle_id) + if not ok: + return HTMLResponse(f"

{msg}

", status_code=400) + return HTMLResponse(status_code=204, headers={"HX-Trigger": "hand-changed"}) + +# Pi-Rat sets a new name +@router.post("/game/{game_id}/player/{player_id}/set-name") +def set_name_route( + game_id: str, + player_id: str, + new_name: str = Form(...), db: Session = Depends(get_session) ): player = crud.get_player(db, player_id) - if not player: - raise HTTPException(status_code=404, detail="Player not found") - - current_status = False - if type == "personal_1": - current_status = player.completed_personal_1 - elif type == "personal_2": - current_status = player.completed_personal_2 - elif type == "personal_3": - current_status = player.completed_personal_3 - - crud.toggle_objective(db, player_id, type, not current_status) + if player and player.needs_name: + player.name = new_name + player.needs_name = False + db.add(player) + db.commit() + return HTMLResponse(status_code=204) + +# Pi-Rat bonus rank up for another player +@router.post("/game/{game_id}/player/{player_id}/bonus-rank-up") +def bonus_rank_up_route( + game_id: str, + player_id: str, + target_player_id: str = Form(...), + db: Session = Depends(get_session) +): + player = crud.get_player(db, player_id) + target = crud.get_player(db, target_player_id) + if player and target and player.needs_rank_3_bonus: + target.rank += 1 + player.needs_rank_3_bonus = False + db.add(target) + db.add(player) + db.commit() + return HTMLResponse(status_code=204) + +# Pi-Rat becomes a ghost +@router.post("/game/{game_id}/player/{player_id}/become-ghost") +def become_ghost_route( + game_id: str, + player_id: str, + db: Session = Depends(get_session) +): + player = crud.get_player(db, player_id) + if player and player.is_dead: + player.is_ghost = True + db.add(player) + db.commit() return HTMLResponse(status_code=204) # Deep ends the scene diff --git a/src/pirats/templates/scene_obstacles_snippet.html b/src/pirats/templates/scene_obstacles_snippet.html index 8131911..cfc56e6 100644 --- a/src/pirats/templates/scene_obstacles_snippet.html +++ b/src/pirats/templates/scene_obstacles_snippet.html @@ -112,11 +112,22 @@ {{ obs.symbol }} {% for pc in column_cards %} -
- {{ pc.card[:-1] if not pc.card.startswith('Joker') else '🃏' }} - {{ parse_card(pc.card).symbol }} - {{ pc.player_name[:4] }} +
+
+ {{ pc.card[:-1] if not pc.card.startswith('Joker') else '🃏' }} + {{ parse_card(pc.card).symbol }} + {{ pc.player_name[:4] }} +
+ {% if loop.last and player.role == 'deep' %} + + {% endif %}
{% endfor %}
diff --git a/src/pirats/templates/scene_partial.html b/src/pirats/templates/scene_partial.html index ca070e7..c28009b 100644 --- a/src/pirats/templates/scene_partial.html +++ b/src/pirats/templates/scene_partial.html @@ -146,13 +146,46 @@
- + {% if player.role == "deep" %} -
+

🌊 Deep Control Panel

+
+

Crew Objectives

+
+ + + +
+
+ +
+

Pi-Rat Personal Objectives

+

You control completion. Only mark in sequence (1 -> 2 -> 3).

+ {% for p in game.players if p.role == 'pirat' %} +
+ {{ p.name }} +
+ + + +
+
+ {% endfor %} +
+ -
+

Conclude the scene once players have made attempts or the obstacle list is depleted.

+ + {% endif %}
- - -
- Bonus: Rank up another player. +
+ {% if player.completed_personal_3 %}✅{% else %}âŦœ{% endif %} â˜ ī¸ Personal 3: Die Like a Pirate +
Bonus: Rank up another player.
+ + {% if player.needs_rank_3_bonus %} +
+ + + +
+ {% endif %} + + {% if player.is_dead and not player.is_ghost %} +
+

You Died!

+

Your Pi-Rat's story has concluded. You may continue to watch over your crew as a ghost!

+ +
+ {% endif %} + {% if player.is_ghost %} +
+ đŸ‘ģ Ghost Mode Active +

You can still use your hand to help the living, but between scenes you may roll a new recruit.

+
+ {% endif %}