Initial commit
This commit is contained in:
324
skills/textual-builder/assets/card-game-template/app.py
Normal file
324
skills/textual-builder/assets/card-game-template/app.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
Card Game Template for Textual
|
||||
|
||||
A starter template for building turn-based card games with Textual.
|
||||
Includes:
|
||||
- Card widget with customizable suit, rank, and face-up/down state
|
||||
- Hand container for displaying player hands
|
||||
- Play area for cards in play
|
||||
- Turn management system
|
||||
- Action system with key bindings
|
||||
|
||||
Customize this template for your specific card game rules.
|
||||
"""
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.widgets import Header, Footer, Label, Static
|
||||
from textual.widget import Widget
|
||||
from textual.reactive import reactive
|
||||
from textual.message import Message
|
||||
from textual.binding import Binding
|
||||
|
||||
|
||||
class Card(Widget):
|
||||
"""A playing card widget."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
border: round white;
|
||||
background: $panel;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
Card:hover {
|
||||
background: $boost;
|
||||
border: heavy $primary;
|
||||
}
|
||||
|
||||
Card.selected {
|
||||
border: double cyan;
|
||||
background: $accent;
|
||||
}
|
||||
|
||||
Card.face-down {
|
||||
background: $surface;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
Card.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.card-rank {
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-suit {
|
||||
text-align: center;
|
||||
}
|
||||
"""
|
||||
|
||||
class Selected(Message):
|
||||
"""Posted when card is selected."""
|
||||
|
||||
def __init__(self, card: "Card") -> None:
|
||||
super().__init__()
|
||||
self.card = card
|
||||
|
||||
class Played(Message):
|
||||
"""Posted when card is played."""
|
||||
|
||||
def __init__(self, card: "Card") -> None:
|
||||
super().__init__()
|
||||
self.card = card
|
||||
|
||||
face_up = reactive(True)
|
||||
selectable = reactive(True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rank: str,
|
||||
suit: str,
|
||||
value: int = 0,
|
||||
face_up: bool = True,
|
||||
card_id: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(id=card_id)
|
||||
self.rank = rank
|
||||
self.suit = suit
|
||||
self.value = value
|
||||
self.face_up = face_up
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
if self.face_up:
|
||||
yield Label(self.rank, classes="card-rank")
|
||||
yield Label(self.suit, classes="card-suit")
|
||||
else:
|
||||
yield Label("🂠", classes="card-back")
|
||||
|
||||
def watch_face_up(self, face_up: bool) -> None:
|
||||
"""Update display when card is flipped."""
|
||||
if face_up:
|
||||
self.remove_class("face-down")
|
||||
else:
|
||||
self.add_class("face-down")
|
||||
# Refresh the card content
|
||||
self.recompose()
|
||||
|
||||
def on_click(self) -> None:
|
||||
"""Handle card click."""
|
||||
if self.selectable:
|
||||
self.post_message(self.Selected(self))
|
||||
|
||||
def flip(self) -> None:
|
||||
"""Flip the card."""
|
||||
self.face_up = not self.face_up
|
||||
|
||||
def play(self) -> None:
|
||||
"""Play this card."""
|
||||
if self.selectable:
|
||||
self.post_message(self.Played(self))
|
||||
|
||||
|
||||
class Hand(Container):
|
||||
"""Container for a player's hand of cards."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Hand {
|
||||
layout: horizontal;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
Hand > Card {
|
||||
margin: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, player_name: str, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.player_name = player_name
|
||||
|
||||
def add_card(self, card: Card) -> None:
|
||||
"""Add a card to this hand."""
|
||||
self.mount(card)
|
||||
|
||||
def remove_card(self, card: Card) -> None:
|
||||
"""Remove a card from this hand."""
|
||||
card.remove()
|
||||
|
||||
def get_cards(self) -> list[Card]:
|
||||
"""Get all cards in this hand."""
|
||||
return list(self.query(Card))
|
||||
|
||||
|
||||
class PlayArea(Container):
|
||||
"""Central area where cards are played."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
PlayArea {
|
||||
height: 1fr;
|
||||
border: round $primary;
|
||||
background: $surface;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
PlayArea > .play-area-label {
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("Play Area", classes="play-area-label")
|
||||
|
||||
|
||||
class GameState(Static):
|
||||
"""Display current game state."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
GameState {
|
||||
dock: top;
|
||||
height: 3;
|
||||
background: $boost;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
}
|
||||
"""
|
||||
|
||||
current_player = reactive("Player 1")
|
||||
turn = reactive(1)
|
||||
|
||||
def render(self) -> str:
|
||||
return f"Turn {self.turn} | Current Player: {self.current_player}"
|
||||
|
||||
|
||||
class CardGameApp(App):
|
||||
"""A card game application."""
|
||||
|
||||
CSS_PATH = "app.tcss"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
("n", "next_turn", "Next Turn"),
|
||||
("d", "draw_card", "Draw Card"),
|
||||
("p", "play_selected", "Play Card"),
|
||||
Binding("space", "toggle_select", "Select/Deselect", show=True),
|
||||
]
|
||||
|
||||
selected_card: Card | None = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets."""
|
||||
yield Header()
|
||||
yield GameState(id="game-state")
|
||||
|
||||
with Vertical(id="game-container"):
|
||||
# Opponent's hand (face down)
|
||||
with Container(id="opponent-area"):
|
||||
yield Label("Opponent", id="opponent-label")
|
||||
yield Hand("Opponent", id="opponent-hand")
|
||||
|
||||
# Play area
|
||||
yield PlayArea(id="play-area")
|
||||
|
||||
# Player's hand (face up)
|
||||
with Container(id="player-area"):
|
||||
yield Hand("Player", id="player-hand")
|
||||
yield Label("Your Hand", id="player-label")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize the game when app starts."""
|
||||
# Deal initial cards (example)
|
||||
player_hand = self.query_one("#player-hand", Hand)
|
||||
opponent_hand = self.query_one("#opponent-hand", Hand)
|
||||
|
||||
# Example: Deal 5 cards to each player
|
||||
suits = ["♠", "♥", "♦", "♣"]
|
||||
ranks = ["A", "2", "3", "4", "5"]
|
||||
|
||||
for i, rank in enumerate(ranks):
|
||||
# Player cards (face up)
|
||||
player_hand.add_card(Card(rank, suits[i % 4], face_up=True, card_id=f"player-{i}"))
|
||||
# Opponent cards (face down)
|
||||
opponent_hand.add_card(Card(rank, suits[i % 4], face_up=False, card_id=f"opp-{i}"))
|
||||
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
"""Handle card selection."""
|
||||
# Deselect previous
|
||||
if self.selected_card:
|
||||
self.selected_card.remove_class("selected")
|
||||
|
||||
# Select new card
|
||||
self.selected_card = event.card
|
||||
event.card.add_class("selected")
|
||||
self.notify(f"Selected {event.card.rank} of {event.card.suit}")
|
||||
|
||||
def on_card_played(self, event: Card.Played) -> None:
|
||||
"""Handle card being played."""
|
||||
play_area = self.query_one("#play-area", PlayArea)
|
||||
card = event.card
|
||||
|
||||
# Remove from hand
|
||||
hand = card.parent
|
||||
if isinstance(hand, Hand):
|
||||
hand.remove_card(card)
|
||||
|
||||
# Move to play area
|
||||
play_area.mount(card)
|
||||
self.notify(f"Played {card.rank} of {card.suit}")
|
||||
|
||||
# Deselect
|
||||
if self.selected_card == card:
|
||||
self.selected_card = None
|
||||
|
||||
def action_next_turn(self) -> None:
|
||||
"""Advance to next turn."""
|
||||
game_state = self.query_one(GameState)
|
||||
game_state.turn += 1
|
||||
|
||||
# Toggle current player
|
||||
if game_state.current_player == "Player 1":
|
||||
game_state.current_player = "Player 2"
|
||||
else:
|
||||
game_state.current_player = "Player 1"
|
||||
|
||||
self.notify(f"Turn {game_state.turn}")
|
||||
|
||||
def action_draw_card(self) -> None:
|
||||
"""Draw a card (example)."""
|
||||
player_hand = self.query_one("#player-hand", Hand)
|
||||
# Example: Draw a random card
|
||||
import random
|
||||
|
||||
suits = ["♠", "♥", "♦", "♣"]
|
||||
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
|
||||
|
||||
card = Card(random.choice(ranks), random.choice(suits), face_up=True)
|
||||
player_hand.add_card(card)
|
||||
self.notify("Drew a card")
|
||||
|
||||
def action_play_selected(self) -> None:
|
||||
"""Play the currently selected card."""
|
||||
if self.selected_card:
|
||||
self.selected_card.play()
|
||||
else:
|
||||
self.notify("No card selected", severity="warning")
|
||||
|
||||
def action_toggle_select(self) -> None:
|
||||
"""Select/deselect hovered card."""
|
||||
# This is a simplified version - in practice you'd track the hovered card
|
||||
if self.selected_card:
|
||||
self.selected_card.remove_class("selected")
|
||||
self.selected_card = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = CardGameApp()
|
||||
app.run()
|
||||
Reference in New Issue
Block a user