use the new `quick_bridge` adapter and do few changes to make TUI work with new backend. TUI will probably be rewritten at some point, this is just an update to keep it working. `urwid` has been updated to have a version working with Python 3.14.
405 lines
15 KiB
Python
405 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
|
|
|
|
# Libervia TUI
|
|
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from libervia.backend.core.i18n import _
|
|
import urwid
|
|
from urwid_satext import sat_widgets
|
|
from libervia.frontends.tools.games import TarotCard
|
|
from libervia.frontends.quick_frontend.quick_game_tarot import QuickTarotGame
|
|
from libervia.tui import xmlui
|
|
from libervia.tui.keys import action_key_map as a_key
|
|
|
|
|
|
class CardDisplayer(urwid.Text):
|
|
"""Show a card"""
|
|
|
|
signals = ["click"]
|
|
|
|
def __init__(self, card):
|
|
self.__selected = False
|
|
self.card = card
|
|
urwid.Text.__init__(self, card.get_attr_text())
|
|
|
|
def selectable(self):
|
|
return True
|
|
|
|
def keypress(self, size, key):
|
|
if key == a_key["CARD_SELECT"]:
|
|
self.select(not self.__selected)
|
|
self._emit("click")
|
|
return key
|
|
|
|
def mouse_event(self, size, event, button, x, y, focus):
|
|
if urwid.is_mouse_event(event) and button == 1:
|
|
self.select(not self.__selected)
|
|
self._emit("click")
|
|
return True
|
|
|
|
return False
|
|
|
|
def select(self, state=True):
|
|
self.__selected = state
|
|
attr, txt = self.card.get_attr_text()
|
|
if self.__selected:
|
|
attr += "_selected"
|
|
self.set_text((attr, txt))
|
|
self._invalidate()
|
|
|
|
def is_selected(self):
|
|
return self.__selected
|
|
|
|
def get_card(self):
|
|
return self.card
|
|
|
|
def render(self, size, focus=False):
|
|
canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus))
|
|
if focus:
|
|
canvas.set_cursor((0, 0))
|
|
return canvas
|
|
|
|
|
|
class Hand(urwid.WidgetWrap):
|
|
"""Used to display several cards, and manage a hand"""
|
|
|
|
signals = ["click"]
|
|
|
|
def __init__(self, hand=[], selectable=False, on_click=None, user_data=None):
|
|
"""@param hand: list of Card"""
|
|
self.__selectable = selectable
|
|
self.columns = urwid.Columns([], dividechars=1)
|
|
if on_click:
|
|
urwid.connect_signal(self, "click", on_click, user_data)
|
|
if hand:
|
|
self.update(hand)
|
|
urwid.WidgetWrap.__init__(self, self.columns)
|
|
|
|
def selectable(self):
|
|
return self.__selectable
|
|
|
|
def keypress(self, size, key):
|
|
|
|
if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]:
|
|
return self.columns.keypress(size, key)
|
|
else:
|
|
# No card displayed, we still have to manage the clicks
|
|
if key == a_key["CARD_SELECT"]:
|
|
self._emit("click", None)
|
|
return key
|
|
|
|
def get_selected(self):
|
|
"""Return a list of selected cards"""
|
|
_selected = []
|
|
for wid in self.columns.widget_list:
|
|
if isinstance(wid, CardDisplayer) and wid.is_selected():
|
|
_selected.append(wid.get_card())
|
|
return _selected
|
|
|
|
def update(self, hand):
|
|
"""Update the hand displayed in this widget
|
|
@param hand: list of Card"""
|
|
try:
|
|
del self.columns.widget_list[:]
|
|
del self.columns.column_types[:]
|
|
except IndexError:
|
|
pass
|
|
self.columns.contents.append((urwid.Text(""), ("weight", 1, False)))
|
|
for card in hand:
|
|
widget = CardDisplayer(card)
|
|
self.columns.widget_list.append(widget)
|
|
self.columns.column_types.append(("fixed", 3))
|
|
urwid.connect_signal(widget, "click", self.__on_click)
|
|
self.columns.contents.append((urwid.Text(""), ("weight", 1, False)))
|
|
self.columns.focus_position = 1
|
|
|
|
def __on_click(self, card_wid):
|
|
self._emit("click", card_wid)
|
|
|
|
|
|
class Card(TarotCard):
|
|
"""This class is used to represent a card, logically
|
|
and give a text representation with attributes"""
|
|
|
|
SIZE = 3 # size of a displayed card
|
|
|
|
def __init__(self, suit, value):
|
|
"""@param file: path of the PNG file"""
|
|
TarotCard.__init__(self, (suit, value))
|
|
|
|
def get_attr_text(self):
|
|
"""return text representation of the card with attributes"""
|
|
try:
|
|
value = "%02i" % int(self.value)
|
|
except ValueError:
|
|
value = self.value[0].upper() + self.value[1]
|
|
if self.suit == "atout":
|
|
if self.value == "excuse":
|
|
suit = "c"
|
|
else:
|
|
suit = "A"
|
|
color = "neutral"
|
|
elif self.suit == "pique":
|
|
suit = "♠"
|
|
color = "black"
|
|
elif self.suit == "trefle":
|
|
suit = "♣"
|
|
color = "black"
|
|
elif self.suit == "coeur":
|
|
suit = "♥"
|
|
color = "red"
|
|
elif self.suit == "carreau":
|
|
suit = "♦"
|
|
color = "red"
|
|
if self.bout:
|
|
color = "special"
|
|
return ("card_%s" % color, "%s%s" % (value, suit))
|
|
|
|
def get_widget(self):
|
|
"""Return a widget representing the card"""
|
|
return CardDisplayer(self)
|
|
|
|
|
|
class Table(urwid.Widget):
|
|
"""Represent the cards currently on the table"""
|
|
|
|
_sizing = frozenset([urwid.Sizing.FLOW])
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.top = self.left = self.bottom = self.right = None
|
|
|
|
def put_card(self, location, card):
|
|
"""Put a card on the table
|
|
@param location: where to put the card (top, left, bottom or right)
|
|
@param card: Card to play or None"""
|
|
assert location in ["top", "left", "bottom", "right"]
|
|
assert isinstance(card, Card) or card == None
|
|
if [getattr(self, place) for place in ["top", "left", "bottom", "right"]].count(
|
|
None
|
|
) == 0:
|
|
# If the table is full of card, we remove them
|
|
self.top = self.left = self.bottom = self.right = None
|
|
setattr(self, location, card)
|
|
self._invalidate()
|
|
|
|
def rows(self, size, focus=False):
|
|
return self.display_widget(size, focus).rows(size, focus)
|
|
|
|
def render(self, size, focus=False):
|
|
return self.display_widget(size, focus).render(size, focus)
|
|
|
|
def display_widget(self, size, focus):
|
|
cards = {}
|
|
(max_col,) = size
|
|
separator = " - "
|
|
margin = max((max_col - Card.SIZE) / 2, 0) * " "
|
|
margin_center = max((max_col - Card.SIZE * 2 - len(separator)) / 2, 0) * " "
|
|
for location in ["top", "left", "bottom", "right"]:
|
|
card = getattr(self, location)
|
|
cards[location] = card.get_attr_text() if card else Card.SIZE * " "
|
|
render_wid = [
|
|
urwid.Text([margin, cards["top"]]),
|
|
urwid.Text([margin_center, cards["left"], separator, cards["right"]]),
|
|
urwid.Text([margin, cards["bottom"]]),
|
|
]
|
|
return urwid.Pile(render_wid)
|
|
|
|
|
|
class TarotGame(QuickTarotGame, urwid.WidgetWrap):
|
|
"""Widget for card games"""
|
|
|
|
def __init__(self, parent, referee, players):
|
|
QuickTarotGame.__init__(self, parent, referee, players)
|
|
self.load_cards()
|
|
self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), "center")])
|
|
# self.parent.host.debug()
|
|
self.table = Table()
|
|
self.center = urwid.Columns(
|
|
[
|
|
("fixed", len(self.left_nick), urwid.Filler(urwid.Text(self.left_nick))),
|
|
urwid.Filler(self.table),
|
|
(
|
|
"fixed",
|
|
len(self.right_nick),
|
|
urwid.Filler(urwid.Text(self.right_nick)),
|
|
),
|
|
]
|
|
)
|
|
"""urwid.Pile([urwid.Padding(self.top_card_wid,'center'),
|
|
urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)),
|
|
urwid.Padding(self.center_cards_wid,'center'),
|
|
('fixed',len(self.right_nick),urwid.Text(self.right_nick))
|
|
]),
|
|
urwid.Padding(self.bottom_card_wid,'center')
|
|
])"""
|
|
self.hand_wid = Hand(selectable=True, on_click=self.on_click)
|
|
self.main_frame = urwid.Frame(
|
|
self.center, header=self.top, footer=self.hand_wid, focus_part="footer"
|
|
)
|
|
urwid.WidgetWrap.__init__(self, self.main_frame)
|
|
self.parent.host.bridge.tarot_game_ready(
|
|
self.player_nick, referee, self.parent.profile
|
|
)
|
|
|
|
def load_cards(self):
|
|
"""Load all the cards in memory"""
|
|
QuickTarotGame.load_cards(self)
|
|
for value in list(map(str, list(range(1, 22)))) + ["excuse"]:
|
|
card = Card("atout", value)
|
|
self.cards[card.suit, card.value] = card
|
|
self.deck.append(card)
|
|
for suit in ["pique", "coeur", "carreau", "trefle"]:
|
|
for value in list(map(str, list(range(1, 11)))) + [
|
|
"valet",
|
|
"cavalier",
|
|
"dame",
|
|
"roi",
|
|
]:
|
|
card = Card(suit, value)
|
|
self.cards[card.suit, card.value] = card
|
|
self.deck.append(card)
|
|
|
|
def tarot_game_new_handler(self, hand):
|
|
"""Start a new game, with given hand"""
|
|
if hand is []: # reset the display after the scores have been showed
|
|
self.reset_round()
|
|
for location in ["top", "left", "bottom", "right"]:
|
|
self.table.put_card(location, None)
|
|
self.parent.host.redraw()
|
|
self.parent.host.bridge.tarot_game_ready(
|
|
self.player_nick, self.referee, self.parent.profile
|
|
)
|
|
return
|
|
QuickTarotGame.tarot_game_new_handler(self, hand)
|
|
self.hand_wid.update(self.hand)
|
|
self.parent.host.redraw()
|
|
|
|
def tarot_game_choose_contrat_handler(self, xml_data):
|
|
"""Called when the player has to select his contrat
|
|
@param xml_data: SàT xml representation of the form"""
|
|
form = xmlui.create(
|
|
self.parent.host,
|
|
xml_data,
|
|
title=_("Please choose your contrat"),
|
|
flags=["NO_CANCEL"],
|
|
profile=self.parent.profile,
|
|
)
|
|
form.show(valign="top")
|
|
|
|
def tarot_game_show_cards_handler(self, game_stage, cards, data):
|
|
"""Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
|
|
QuickTarotGame.tarot_game_show_cards_handler(self, game_stage, cards, data)
|
|
self.center.widget_list[1] = urwid.Filler(Hand(self.to_show))
|
|
self.parent.host.redraw()
|
|
|
|
def tarot_game_your_turn_handler(self):
|
|
QuickTarotGame.tarot_game_your_turn_handler(self)
|
|
|
|
def tarot_game_score_handler(self, xml_data, winners, loosers):
|
|
"""Called when the round is over, display the scores
|
|
@param xml_data: SàT xml representation of the form"""
|
|
if not winners and not loosers:
|
|
title = _("Draw game")
|
|
else:
|
|
title = _("You win \\o/") if self.player_nick in winners else _("You loose :(")
|
|
form = xmlui.create(
|
|
self.parent.host,
|
|
xml_data,
|
|
title=title,
|
|
flags=["NO_CANCEL"],
|
|
profile=self.parent.profile,
|
|
)
|
|
form.show()
|
|
|
|
def tarot_game_invalid_cards_handler(self, phase, played_cards, invalid_cards):
|
|
"""Invalid cards have been played
|
|
@param phase: phase of the game
|
|
@param played_cards: all the cards played
|
|
@param invalid_cards: cards which are invalid"""
|
|
QuickTarotGame.tarot_game_invalid_cards_handler(
|
|
self, phase, played_cards, invalid_cards
|
|
)
|
|
self.hand_wid.update(self.hand)
|
|
if self._autoplay == None: # No dialog if there is autoplay
|
|
self.parent.host.bar_notify(_("Cards played are invalid !"))
|
|
self.parent.host.redraw()
|
|
|
|
def tarot_game_cards_played_handler(self, player, cards):
|
|
"""A card has been played by player"""
|
|
QuickTarotGame.tarot_game_cards_played_handler(self, player, cards)
|
|
self.table.put_card(self.get_player_location(player), self.played[player])
|
|
self._checkState()
|
|
self.parent.host.redraw()
|
|
|
|
def _checkState(self):
|
|
if isinstance(
|
|
self.center.widget_list[1].original_widget, Hand
|
|
): # if we have a hand displayed
|
|
self.center.widget_list[1] = urwid.Filler(
|
|
self.table
|
|
) # we show again the table
|
|
if self.state == "chien":
|
|
self.to_show = []
|
|
self.state = "wait"
|
|
elif self.state == "wait_for_ecart":
|
|
self.state = "ecart"
|
|
self.hand.extend(self.to_show)
|
|
self.hand.sort()
|
|
self.to_show = []
|
|
self.hand_wid.update(self.hand)
|
|
|
|
##EVENTS##
|
|
def on_click(self, hand, card_wid):
|
|
"""Called when user do an action on the hand"""
|
|
if not self.state in ["play", "ecart", "wait_for_ecart"]:
|
|
# it's not our turn, we ignore the click
|
|
card_wid.select(False)
|
|
return
|
|
self._checkState()
|
|
if self.state == "ecart":
|
|
if len(self.hand_wid.get_selected()) == 6:
|
|
pop_up_widget = sat_widgets.ConfirmDialog(
|
|
_("Do you put these cards in chien ?"),
|
|
yes_cb=self.on_ecart_done,
|
|
no_cb=self.parent.host.remove_pop_up,
|
|
)
|
|
self.parent.host.show_pop_up(pop_up_widget)
|
|
elif self.state == "play":
|
|
card = card_wid.get_card()
|
|
self.parent.host.bridge.tarot_game_play_cards(
|
|
self.player_nick,
|
|
self.referee,
|
|
[(card.suit, card.value)],
|
|
self.parent.profile,
|
|
)
|
|
self.hand.remove(card)
|
|
self.hand_wid.update(self.hand)
|
|
self.state = "wait"
|
|
|
|
def on_ecart_done(self, button):
|
|
"""Called when player has finished his écart"""
|
|
ecart = []
|
|
for card in self.hand_wid.get_selected():
|
|
ecart.append((card.suit, card.value))
|
|
self.hand.remove(card)
|
|
self.hand_wid.update(self.hand)
|
|
self.parent.host.bridge.tarot_game_play_cards(
|
|
self.player_nick, self.referee, ecart, self.parent.profile
|
|
)
|
|
self.state = "wait"
|
|
self.parent.host.remove_pop_up()
|