ChessCommanderFlet / flet_app.py
Danisz's picture
Camera working on JS
6cbbb50
Raw History Blame Contribute Delete
49.3 kB
import flet as ft
import chess
from domain.chess_piece_model import ChessPiece, PieceType, PieceID
from domain.translations import spanish_translations
from domain.localapi_dialog_processor import LocalApiDialogueProcessor
import logging
import asyncio
from typing import Optional, List, Callable, Tuple
from audio_utils import get_base64_audio
from pydantic import BaseModel, Field
from camera_utils import CameraImage
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class EmpathyConfig:
INITIAL_EMPATHY = 0.5
INITIAL_EMPATHY_PAWN = 0.1
INITIAL_EMPATHY_OTHER_PIECES = 0.8
DECREASE_AMOUNT = 0.02
MIN_EMPATHY = 0.05
HIGH_EMPATHY_THRESHOLD = 0.9
FREE_MOVE_EMPATHY = 0.5
STEP_AMOUNT = 0.2
class ChessModel:
def __init__(self):
self.board = chess.Board()
self.piece_mapping = self._initialize_piece_mapping()
self.last_move = None
def is_king_empathy_zero_or_below(self) -> bool:
for piece in self.piece_mapping.values():
if piece.piece_type == PieceType.KING and piece.empathy <= 0.0:
return True
return False
def are_all_pieces_empathy_above(self, threshold: float) -> bool:
print(f"Checking if all pieces have empathy above {threshold}")
result = all(piece.empathy >= threshold for piece in self.piece_mapping.values())
print(f"All pieces above threshold: {result}")
return result
def remove_piece(self, square: chess.Square):
piece = self.piece_mapping.pop(square, None)
if piece:
self.board.remove_piece_at(square)
self.last_move = chess.Move(square, square)
print(f"Piece at {chess.SQUARE_NAMES[square]} has been eliminated")
else:
print(f"No piece at {chess.SQUARE_NAMES[square]} to remove")
def _initialize_piece_mapping(self):
mapping = {}
white_pieces = [
PieceID.WhiteRook1, PieceID.WhiteKnight1, PieceID.WhiteBishop1, PieceID.WhiteQueen, PieceID.WhiteKing,
PieceID.WhiteBishop2, PieceID.WhiteKnight2, PieceID.WhiteRook2,
PieceID.WhitePawn1, PieceID.WhitePawn2, PieceID.WhitePawn3, PieceID.WhitePawn4,
PieceID.WhitePawn5, PieceID.WhitePawn6, PieceID.WhitePawn7, PieceID.WhitePawn8
]
black_pieces = [
PieceID.WhitePawn1, PieceID.WhitePawn2, PieceID.WhitePawn3, PieceID.WhitePawn4,
PieceID.WhitePawn5, PieceID.WhitePawn6, PieceID.WhitePawn7, PieceID.WhitePawn8,
PieceID.WhiteRook1, PieceID.WhiteKnight1, PieceID.WhiteBishop1, PieceID.WhiteQueen, PieceID.WhiteKing,
PieceID.WhiteBishop2, PieceID.WhiteKnight2, PieceID.WhiteRook2,
]
for square in chess.SQUARES:
piece = self.board.piece_at(square)
if piece:
piece_type = PieceType(piece.piece_type)
color = piece.color
if color == chess.WHITE:
piece_id = white_pieces.pop(0)
else:
piece_id = black_pieces.pop(0)
chess_piece = ChessPiece(piece_type, color, piece_id)
if piece_type == PieceType.PAWN:
chess_piece.empathy = EmpathyConfig.INITIAL_EMPATHY_PAWN
else:
chess_piece.empathy = EmpathyConfig.INITIAL_EMPATHY_OTHER_PIECES
mapping[square] = chess_piece
logger.info(f"Piece at {chess.SQUARE_NAMES[square]}: {chess_piece} (Empathy: {chess_piece.empathy:.2f})")
return mapping
def get_piece_at(self, square: chess.Square):
chess_piece = self.piece_mapping.get(square)
if chess_piece:
return chess_piece.symbol, chess_piece
return " ", None
def make_move(self, move: chess.Move) -> bool:
if move in self.board.legal_moves:
from_square = move.from_square
to_square = move.to_square
moving_piece = self.piece_mapping.get(from_square)
if moving_piece:
logger.info(f"Moving {moving_piece} from {chess.SQUARE_NAMES[from_square]} to {chess.SQUARE_NAMES[to_square]} (Empathy: {moving_piece.empathy:.2f})")
captured_piece = self.piece_mapping.get(to_square)
if captured_piece:
logger.info(f"Captured {captured_piece} at {chess.SQUARE_NAMES[to_square]} (Empathy: {captured_piece.empathy:.2f})")
# Handle pawn promotion
if move.promotion:
promoted_piece_type = PieceType(move.promotion)
promoted_piece = ChessPiece(promoted_piece_type, moving_piece.color)
promoted_piece.empathy = moving_piece.empathy # Preserve empathy
self.piece_mapping[to_square] = promoted_piece
logger.info(f"Pawn promoted to {promoted_piece_type} at {chess.SQUARE_NAMES[to_square]}")
# Remove the pawn from the original square
del self.piece_mapping[from_square]
else:
# For non-promotion moves, update the piece's position
self.piece_mapping[to_square] = self.piece_mapping.pop(from_square)
# Handle castling
if self.board.is_castling(move):
if to_square > from_square: # Kingside castling
rook_from = chess.H1 if self.board.turn == chess.WHITE else chess.H8
rook_to = chess.F1 if self.board.turn == chess.WHITE else chess.F8
else: # Queenside castling
rook_from = chess.A1 if self.board.turn == chess.WHITE else chess.A8
rook_to = chess.D1 if self.board.turn == chess.WHITE else chess.D8
# Move the rook
self.piece_mapping[rook_to] = self.piece_mapping.pop(rook_from)
logger.info(f"Castling: Rook moved from {chess.SQUARE_NAMES[rook_from]} to {chess.SQUARE_NAMES[rook_to]}")
self.board.push(move)
self.last_move = move
self.decrease_all_pieces_empathy()
return True
return False
def decrease_all_pieces_empathy(self, decrease_amount=EmpathyConfig.DECREASE_AMOUNT):
for square, piece in self.piece_mapping.items():
if piece.empathy > EmpathyConfig.MIN_EMPATHY and piece.empathy < EmpathyConfig.HIGH_EMPATHY_THRESHOLD:
piece.empathy = max(EmpathyConfig.MIN_EMPATHY, piece.empathy - decrease_amount)
logger.info(f"Decreased empathy for {piece} at {chess.SQUARE_NAMES[square]} to {piece.empathy:.2f}")
def is_white_to_move(self) -> bool:
return self.board.turn == chess.WHITE
def is_promotion_move(self, move: chess.Move) -> bool:
piece = self.board.piece_at(move.from_square)
return (piece is not None and piece.piece_type == chess.PAWN and
((piece.color == chess.WHITE and chess.square_rank(move.to_square) == 7) or
(piece.color == chess.BLACK and chess.square_rank(move.to_square) == 0)))
def is_checkmate(self) -> bool:
return self.board.is_checkmate()
def reset_game(self):
self.board.reset()
self.piece_mapping = self._initialize_piece_mapping()
self.last_move = None
logger.info("Game reset. New piece empathy values:")
for square, piece in self.piece_mapping.items():
logger.info(f"Piece at {chess.SQUARE_NAMES[square]}: {piece} (Empathy: {piece.empathy:.2f})")
class StreamConfig(BaseModel):
max_width: int = Field(default=320, gt=0)
fps_target: int = Field(default=40, gt=0)
jpeg_quality: int = Field(default=40, ge=0, le=100)
class RecordingCountdown(ft.ProgressBar):
def __init__(self, seconds):
super().__init__()
self.seconds = seconds
self.value = 1.0
def did_mount(self):
self.running = True
self.page.run_task(self.update_timer)
def will_unmount(self):
self.running = False
async def update_timer(self):
start_time = asyncio.get_event_loop().time()
while self.seconds > 0 and self.running:
elapsed_time = asyncio.get_event_loop().time() - start_time
remaining_time = max(0, self.seconds - elapsed_time)
self.value = remaining_time / self.seconds
self.update()
if remaining_time <= 0:
break
await asyncio.sleep(0.1)
class ChessPresenter:
def __init__(self, view: 'ChessView'):
self.view = view
self.model = ChessModel()
self.dialogue_processor = LocalApiDialogueProcessor("https://danisz-chesscommanderservice.hf.space") # "https://danisz-chesscommanderservice.hf.space"
self.selected_square = None
self.pending_move = None
self.valid_moves = []
self.audio_file_path = None
self.video_file_path = None
self.current_sprite = None
self.last_move_sprite = None
self.last_move_response = None
self.elimination_mode = False
self.recording_timer = None
self.countdown_timer = None
def toggle_video_recording(self):
if self.view.is_camera_active():
self.stop_video_recording()
self.view.show_processing_feedback()
else:
self.start_video_recording()
def start_video_recording(self):
self.view.start_camera_stream()
self.view.show_video_container()
self.view.update_video_button(is_recording=True)
def stop_video_recording(self):
self.view.stop_camera_stream()
self.view.update_video_button(is_recording=False)
self.view.hide_video_container()
def process_audio_move_argument(self, audio_base64: str):
piece_symbol, chess_piece = self.model.get_piece_at(self.pending_move.from_square)
if chess_piece is not None:
logger.info(f"Processing audio move for {chess_piece} (Empathy: {chess_piece.empathy:.2f})")
dialogue_response = self.dialogue_processor.process_audio_input(
chess_piece.piece_id,
audio_base64,
self.view.current_language,
chess_piece.empathy
)
logger.info(f"Dialogue Processor Response: {dialogue_response.response}")
logger.info(f"Audio Transcription: {dialogue_response.transcription}")
self.view.show_response(dialogue_response.response, piece_symbol, dialogue_response.transcription)
self.view.update_piece_voice_attributes(dialogue_response.piece_speak_velocity)
self.view.update_player_voice_attributes(dialogue_response.transcription, dialogue_response.player_speak_velocity)
self.view.hide_processing_feedback()
self.view.hide_move_argument_dialog()
self.handle_move_decision(dialogue_response.decision, chess_piece)
if dialogue_response.audio_data:
mime_type = 'audio/wav'
audio_url = f"data:{mime_type};base64,{dialogue_response.audio_data}"
self.view.play_audio(audio_url)
else:
logger.error("Error: No valid piece selected for the move.")
self.pending_move = None
def process_text_move_argument(self, text_input: str):
piece_symbol, chess_piece = self.model.get_piece_at(self.pending_move.from_square)
if chess_piece is not None:
dialogue_response = self.dialogue_processor.process_text_input(
chess_piece.piece_id,
text_input,
self.view.current_language,
chess_piece.empathy
)
logger.info(f"Dialogue Processor Response: {dialogue_response.response}")
logger.info(f"Transcription: {dialogue_response.transcription}")
self.view.show_response(dialogue_response.response, piece_symbol, dialogue_response.transcription)
self.view.update_player_voice_attributes(None)
self.view.hide_processing_feedback()
self.view.hide_move_argument_dialog()
self.handle_move_decision(dialogue_response.decision, chess_piece)
if dialogue_response.audio_data:
mime_type = 'audio/wav'
audio_url = f"data:{mime_type};base64,{dialogue_response.audio_data}"
self.view.play_audio(audio_url)
else:
logger.error("Error: No valid piece selected for the move.")
self.pending_move = None
def adjust_empathy(self, chess_piece: ChessPiece, increase: bool):
if increase:
chess_piece.empathy = min(chess_piece.empathy + EmpathyConfig.STEP_AMOUNT, 1.0)
else:
chess_piece.empathy = max(chess_piece.empathy - EmpathyConfig.STEP_AMOUNT, 0.0)
if self.model.is_king_empathy_zero_or_below():
loser = "White" if self.model.is_white_to_move() else "Black"
logger.info(f"{loser} loses because the king's empathy is zero or below.")
self.view.show_king_empathy_loss_dialog(loser)
def handle_move_decision(self, decision: bool, chess_piece: ChessPiece):
if decision:
self.make_move(self.pending_move)
self.adjust_empathy(chess_piece, True)
else:
logger.info("Move rejected based on dialogue processor decision.")
self.adjust_empathy(chess_piece, False)
self.check_and_remove_piece(self.pending_move.from_square)
# Change the turn from white to black or vice versa
self.model.board.turn = not self.model.board.turn
self.view.switch_turn()
# Decrease empathy for all pieces after each turn
self.model.decrease_all_pieces_empathy()
def check_and_remove_piece(self, square: chess.Square):
piece = self.model.piece_mapping.get(square)
if piece and piece.empathy <= 0:
self.model.remove_piece(square)
self.update_board()
def make_move(self, move: chess.Move):
if self.model.make_move(move):
from_square = move.from_square
to_square = move.to_square
moving_piece = self.model.piece_mapping.get(to_square)
if moving_piece:
if moving_piece.empathy >= EmpathyConfig.HIGH_EMPATHY_THRESHOLD:
moving_piece.empathy = EmpathyConfig.FREE_MOVE_EMPATHY
logger.info(f"Piece at {chess.SQUARE_NAMES[to_square]} made a free move. Empathy reset to {EmpathyConfig.FREE_MOVE_EMPATHY}")
was_promotion = move.promotion is not None
self.update_board(from_square, to_square, was_promotion)
if self.model.is_checkmate():
winner = "White" if not self.model.is_white_to_move() else "Black"
logger.info(f"Checkmate! {winner} wins.")
self.view.show_checkmate_dialog(winner)
else:
self.view.switch_turn()
self.on_turn_begin()
else:
logger.warning(f"Invalid move: {move}")
def on_text_submit(self, text: str):
self.process_text_move_argument(text)
def on_view_initialized(self):
logger.info("Chess game initialized. Current board state:")
self.update_board()
for square, piece in self.model.piece_mapping.items():
logger.info(f"Piece at {chess.SQUARE_NAMES[square]}: {piece} (Empathy: {piece.empathy:.2f})")
self.on_turn_begin() # Make sure this is called at the start of the game
def update_board(self, from_square=None, to_square=None, was_promotion=False):
board_state = self._get_board_state()
last_move_square = self.model.last_move.to_square if self.model.last_move else None
self.view.draw_board(board_state, self.selected_square, self.valid_moves, last_move_square, from_square, to_square, was_promotion)
def _get_board_state(self):
return [[self.model.get_piece_at(chess.square(col, 7 - row))[0] for col in range(8)] for row in range(8)]
def on_square_click(self, row, col):
square = chess.square(col, 7 - row)
piece, piece_enum = self.model.get_piece_at(square)
is_valid_piece = (piece != " " and
((self.model.is_white_to_move() and piece.isupper()) or
(not self.model.is_white_to_move() and piece.islower())))
print(f"Click: Piece={piece}, self.model.is_white_to_move()={self.model.is_white_to_move()}")
if is_valid_piece:
self.selected_square = square
self.valid_moves = [move.to_square for move in self.model.board.legal_moves if move.from_square == square]
logger.info(f"Selected piece at {chess.SQUARE_NAMES[square]}: {piece}")
self.current_sprite = piece
self.view.show_empathy_bar(piece_enum.empathy)
elif self.selected_square is not None:
move = chess.Move(self.selected_square, square)
if self.model.is_promotion_move(move):
move = chess.Move(self.selected_square, square, promotion=chess.QUEEN)
if self.model.board.is_legal(move):
_, moving_piece = self.model.get_piece_at(self.selected_square)
if moving_piece and moving_piece.empathy >= 0.9:
self.make_move(move)
else:
self.pending_move = move
self.view.show_move_argument_dialog()
else:
logger.info(f"Invalid move: {move}")
self.selected_square = None
self.valid_moves = []
self.update_board()
def restart_game(self):
self.model.reset_game()
self.selected_square = None
self.pending_move = None
self.valid_moves = []
self.update_board()
self.view.hide_checkmate_dialog()
self.view.reset_turn()
self.view.hide_sprite_and_dialogue()
self.on_turn_begin() # Make sure this is called when the game is restarted
def on_turn_begin(self):
print("on_turn_begin: Method called")
logger.info("Checking piece empathy levels...")
self.view.hide_player_transcription()
all_high_empathy = self.model.are_all_pieces_empathy_above(EmpathyConfig.HIGH_EMPATHY_THRESHOLD)
print(f"All pieces have high empathy: {all_high_empathy}")
if all_high_empathy:
self.view.show_eliminate_piece_popup()
else:
print("Not all pieces have high empathy. Continuing normal gameplay.")
# Log the empathy of all pieces
for square, piece in self.model.piece_mapping.items():
print(f"Piece at {chess.SQUARE_NAMES[square]}: {piece} (Empathy: {piece.empathy:.2f})")
def start_piece_elimination(self):
self.elimination_mode = True
self.update_board() # Use the presenter's method to update the board
def eliminate_rival_piece(self, square: chess.Square):
if not self.elimination_mode:
return
piece = self.model.board.piece_at(square)
if piece and piece.color != self.model.board.turn:
# Ensure that the piece to be eliminated is not a king
if piece.piece_type == chess.KING:
logger.info("Cannot eliminate the king piece.")
self.view.show_error_message("You cannot eliminate the king piece.")
return
self.model.remove_piece(square)
self.update_board()
self.elimination_mode = False
# Switch turn after elimination
self.model.board.turn = not self.model.board.turn
self.view.switch_turn()
self.model.decrease_all_pieces_empathy()
# Reset selection and valid moves
self.selected_square = None
self.valid_moves = []
# Call on_turn_begin to set up for the next turn
self.on_turn_begin()
else:
logger.info("Invalid piece selection for elimination.")
self.view.show_error_message("Invalid piece selection for elimination.")
class ChessView(ft.Container):
def __init__(self, input_page):
super().__init__()
self.my_page = input_page
self.presenter = ChessPresenter(self)
self.sprite_visible = False
self.elimination_mode = False
self.processing_indicator = ft.ProgressRing(visible=False)
self.processing_text = ft.Text("Processing...", visible=False)
self.current_language = "en"
self.en_button = ft.ElevatedButton("EN", on_click=self.set_english)
self.es_button = ft.ElevatedButton("ES", on_click=self.set_spanish)
self.piece_speak_velocity_bar = self.speak_velocity_bar()
self.player_speak_velocity_bar = self.speak_velocity_bar()
self.empathy_bar_visible = True
self.language_selector = ft.Row(
[self.en_button, self.es_button],
alignment=ft.MainAxisAlignment.CENTER
)
self.eliminate_piece_dialog = ft.AlertDialog(
title=ft.Text("Eliminate a Rival Piece"),
content=ft.Text("Do you want to eliminate a rival piece?"),
actions=[
ft.ElevatedButton("Accept", on_click=self.accept_elimination),
ft.ElevatedButton("Cancel", on_click=self.hide_eliminate_piece_popup),
],
actions_alignment=ft.MainAxisAlignment.END,
)
self.board_size = 352
self.square_size = self.board_size // 8
self.selected_color = ft.colors.YELLOW_100
self.valid_move_color = ft.colors.AMBER_200
self.last_move_color = ft.colors.LIGHT_GREEN_200
self.empathy_bar = ft.ProgressBar(
width=self.board_size,
height=10,
color=ft.colors.BLUE,
bgcolor=ft.colors.GREY_300,
value=0.5,
)
self.empathy_text = ft.Text("Empathy", size=14, color=ft.colors.WHITE)
self.empathy_container = ft.Container(
content=ft.Column([
self.empathy_text,
self.empathy_bar
], spacing=5, alignment=ft.MainAxisAlignment.CENTER),
width=self.board_size,
padding=10,
bgcolor=ft.colors.BLACK,
border_radius=ft.border_radius.all(10),
visible=False, # Start hidden
)
self.countdown = RecordingCountdown(10)
self.piece_images = {
'P': '/images/sprites/pawn.png',
'R': '/images/sprites/rook.png',
'N': '/images/sprites/knight.png',
'B': '/images/sprites/bishop.png',
'Q': '/images/sprites/queen.png',
'K': '/images/sprites/king.png',
}
self.chessboardpiece_images = {
'P': '/images/imgs-80px/white_pawn.gif',
'R': '/images/imgs-80px/white_rook.gif',
'N': '/images/imgs-80px/white_knight.gif',
'B': '/images/imgs-80px/white_bishop.gif',
'Q': '/images/imgs-80px/white_queen.gif',
'K': '/images/imgs-80px/white_king.gif',
'p': '/images/imgs-80px/black_pawn.gif',
'r': '/images/imgs-80px/black_rook.gif',
'n': '/images/imgs-80px/black_knight.gif',
'b': '/images/imgs-80px/black_bishop.gif',
'q': '/images/imgs-80px/black_queen.gif',
'k': '/images/imgs-80px/black_king.gif',
}
self.audio_recorder = ft.AudioRecorder(
audio_encoder=ft.AudioEncoder.OPUS,
on_state_changed=self.on_recorder_state_changed
)
self.move_sound = ft.Audio(src="/sounds/move.wav")
self.capture_sound = ft.Audio(src="/sounds/capture.wav")
# Add audio playback component
self.audio_player = ft.Audio(
src="X", # Dummy non-empty string for Flutter dart internals
autoplay=False,
on_loaded=lambda _: print("Loaded"),
on_duration_changed=lambda e: print("Duration changed:", e.data),
on_position_changed=lambda e: print("Position changed:", e.data),
on_state_changed=lambda e: clean_audio_player(e),
on_seek_complete=lambda _: print("\nSeek complete"),
)
def clean_audio_player(e):
print("State changed:", e.data)
if e.data == "completed":
self.audio_player.autoplay = False
self.audio_player.update()
self.camera_image = CameraImage(width=320, height=240)
self.video_container = ft.Container(
content=self.camera_image,
visible=False,
)
# Adjust the dialogue size
self.dialogue_height = 800 # Increased height
self.dialogue_width = 840 # Increased width
# Adjust the sprite size
self.sprite_placeholder = ft.Image(
src="",
width=800,
height=800,
fit=ft.ImageFit.CONTAIN
)
# Adjust the response text and dialogue
self.response_text = ft.Text("", size=18, color=ft.colors.WHITE)
self.piece_dialogue = ft.Container(
content=self.response_text,
bgcolor=ft.colors.BLACK, # Semi-transparent background
padding=10,
width=360,
height=100,
border_radius=ft.border_radius.all(10),
border=ft.border.all(color=ft.colors.YELLOW, width=2),
)
# Define response_title_container
self.response_title_container = ft.Container(
content=ft.Text("Response", color=ft.colors.WHITE, size=14),
bgcolor=ft.colors.BLUE,
padding=5,
border_radius=ft.border_radius.all(10),
)
# Adjust the response container
self.response_container = ft.Stack(
controls=[
self.piece_dialogue,
ft.Container(self.response_title_container, alignment=ft.alignment.bottom_left, padding=ft.Padding(left=20, top=-10, right=0, bottom=0)),
],
width=320, # Increased width
)
# Adjust the sprite_and_dialogue_container
self.response_layout = ft.Container(
content=None, # We'll set this in show_sprite_and_dialogue
alignment=ft.alignment.bottom_center,
bottom=20, # Distance from bottom of screen
left=0,
right=0,
)
self.chessboard = ft.Container(
width=self.board_size,
height=self.board_size,
content=ft.Column(spacing=0),
bgcolor=None,
)
# Add these lines to create the transcription elements
self.transcription_text = ft.Text("", size=14, color=ft.colors.WHITE)
self.transcription_container = ft.Container(
content=self.transcription_text,
bgcolor=ft.colors.BLACK54,
padding=10,
border_radius=ft.border_radius.all(10),
visible=False
)
self.built_control = self.build_ui()
self.setup_ui(self.built_control)
self.current_turn = chess.WHITE
def initialize_view(self):
self.update_turn_indicator()
self.presenter.on_view_initialized()
def play_audio(self, file_path: str):
print("Playing audio")
self.audio_player.src = file_path
self.audio_player.autoplay = True
self.audio_player.update()
def set_english(self, e):
self.current_language = "en"
self.update_language()
def set_spanish(self, e):
self.current_language = "es"
self.update_language()
def update_language(self):
self.update_ui_texts()
self.update()
def update_ui_texts(self):
self.checkmate_dialog.title.value = self.get_localized_string("Checkmate!")
self.submit_button.text = self.get_localized_string("Submit")
self.move_argument_dialog.title.value = self.get_localized_string("Describe your move")
self.text_input.label = self.get_localized_string("Enter your move description")
self.record_button.tooltip = self.get_localized_string("Record move argument")
self.move_argument_text.value = self.get_localized_string("Type your move or use the microphone to record")
self.processing_text.value = self.get_localized_string("Processing...")
self.update_turn_indicator()
def get_localized_string(self, key):
if self.current_language == "es":
return spanish_translations.get(key, key)
return key
def show_processing_feedback(self):
self.move_argument_text.visible = False
self.text_input.visible = False
self.submit_button.visible = False
self.record_button.visible = False
self.video_record_button.visible = False
self.processing_indicator.visible = True
self.processing_text.visible = True
self.update()
def hide_processing_feedback(self):
self.move_argument_text.visible = True
self.text_input.visible = True
self.submit_button.visible = True
self.record_button.visible = True
self.video_record_button.visible = True
self.processing_indicator.visible = False
self.processing_text.visible = False
self.update()
def setup_ui(self, control: ft.Control):
self.width = 360
self.height = 800
self.bgcolor = ft.colors.RED # Debugging
self.alignment = ft.alignment.center
self.expand = False
background_image_container = ft.Container(
image_src='/images/chess_background.jpg',
image_fit=ft.ImageFit.COVER,
alignment=ft.alignment.center,
bgcolor=ft.colors.BLUE, # Debugging
expand=True
)
self.content = ft.Stack(
controls=[
background_image_container,
control
],
alignment=ft.alignment.center
)
def did_mount(self):
self.presenter.on_view_initialized()
def show_sprite(self, piece: str):
piece_image_path = self.piece_images.get(piece.upper(), "")
self.sprite_placeholder.src = piece_image_path
self.hide_sprite_and_dialogue()
self.update()
def draw_board(self, board, selected_square, valid_moves, last_move_square, from_square=None, to_square=None, was_promotion=False):
rows = []
for row in range(8):
board_row = ft.Row(spacing=0)
for col in range(8):
square = chess.square(col, 7 - row)
if selected_square is not None and square == selected_square:
color = self.selected_color
elif square in valid_moves:
color = self.valid_move_color
elif square == last_move_square:
color = self.last_move_color
else:
color = ft.colors.ORANGE_100 if (row + col) % 2 == 0 else ft.colors.ORANGE_300
piece, chess_piece = self.presenter.model.get_piece_at(square)
if was_promotion and square == from_square:
piece = " "
chess_piece = None
high_empathy = chess_piece is not None and chess_piece.empathy >= 0.9
square_container = self.create_square(row, col, color, piece, chess_piece, high_empathy)
board_row.controls.append(square_container)
rows.append(board_row)
self.chessboard.content = ft.Column(rows, spacing=0)
self.update()
def create_square(self, row, col, color, piece, chess_piece, high_empathy=False):
piece_image_path = ""
if high_empathy:
color = ft.colors.WHITE
if chess_piece:
if chess_piece.piece_type == PieceType.QUEEN and piece.lower() == 'q':
piece_image_path = self.chessboardpiece_images.get('Q' if chess_piece.color == chess.WHITE else 'q', "")
else:
piece_image_path = self.chessboardpiece_images.get(piece, "")
piece_image = None
if piece_image_path and not (chess_piece and chess_piece.is_disappeared()):
# Add a scaling factor for the knight
scale = 1.2 if piece.lower() == 'n' else 1.0
piece_image = ft.Image(
src=piece_image_path,
width=self.square_size * scale,
height=self.square_size * scale,
fit=ft.ImageFit.CONTAIN
)
def on_square_click(e):
self.on_square_click(row, col)
return ft.Container(
width=self.square_size,
height=self.square_size,
bgcolor=color,
border=ft.border.all(1, ft.colors.BLACK),
border_radius=ft.border_radius.all(0),
alignment=ft.alignment.center,
data=f"{row},{col}",
on_click=on_square_click,
content=piece_image,
)
def show_eliminate_piece_popup(self):
self.elimination_mode = True
self.eliminate_piece_dialog.open = True
self.update()
def hide_eliminate_piece_popup(self, _=None):
self.elimination_mode = False
self.eliminate_piece_dialog.open = False
self.update()
def exit_elimination_mode(self):
self.elimination_mode = False
self.update()
def show_king_empathy_loss_dialog(self, loser: str):
self.my_page.run_task(self._show_king_empathy_loss_dialog, loser)
async def _show_king_empathy_loss_dialog(self, loser: str):
await asyncio.sleep(1)
loser_text = self.get_localized_string(loser)
lose_reason = self.get_localized_string('loses because the king\'s empathy is zero or below!')
message = f"{loser_text} {lose_reason}"
self.king_empathy_loss_dialog.content.value = message
self.king_empathy_loss_dialog.open = True
self.update()
def update_video_button(self, is_recording: bool):
self.video_record_button.icon = ft.icons.STOP if is_recording else ft.icons.VIDEO_CALL
self.video_record_button.tooltip = "Stop recording" if is_recording else "Record video move argument"
self.update()
def show_response(self, response, piece: str, transcription: str):
self.response_text.value = response
self.show_sprite_and_dialogue(piece)
if transcription:
self.transcription_text.value = transcription
self.transcription_container.visible = True
self.update()
def hide_move_argument_dialog(self):
self.move_argument_dialog.open = False
self.update()
def show_move_argument_dialog(self):
self.move_argument_dialog.open = True
self.text_input.focus()
self.update()
def show_checkmate_dialog(self, winner: str):
winner_text = self.get_localized_string(winner)
message = f"{winner_text} {self.get_localized_string('wins by checkmate!')}"
self.checkmate_dialog.content.value = message
self.checkmate_dialog.open = True
self.update()
def hide_checkmate_dialog(self):
self.checkmate_dialog.open = False
self.update()
def restart_game(self, _=None):
self.checkmate_dialog.open = False
self.king_empathy_loss_dialog.open = False
self.update()
self.presenter.restart_game()
def start_countdown(self):
self.countdown.seconds = 10
self.countdown.value = 1.0
self.countdown.running = True
self.countdown.did_mount()
self.update()
def stop_countdown(self):
self.countdown.running = False
self.countdown.will_unmount()
self.update()
async def toggle_recording(self, e):
if await self.audio_recorder.is_recording_async():
await self.stop_recording()
else:
await self.start_recording()
async def start_recording(self):
import tempfile
self.record_button.disabled = True
self.update()
temp_recording_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
print(f"Starting recording to {temp_recording_file.name}")
started = await self.audio_recorder.invoke_method_async(
"start_recording",
{"outputPath": temp_recording_file.name},
wait_for_result=True,
wait_timeout=10
)
if started == "true":
print("Recording started successfully")
self.record_button.icon = ft.icons.STOP
self.record_button.tooltip = "Stop recording"
self.record_button.disabled = False
self.start_countdown()
else:
print("Failed to start recording")
self.record_button.disabled = False
self.update()
async def stop_recording(self):
self.stop_countdown()
self.show_processing_feedback()
self.update()
audio_uri = await self.audio_recorder.stop_recording_async()
print(f"Recording stopped. URI: {audio_uri}")
audio_base64 = await self.get_base64_audio(audio_uri)
print("Base64 representation (first 50 chars):", audio_base64[:50])
self.record_button.icon = ft.icons.MIC
self.record_button.tooltip = "Record move argument"
self.presenter.process_audio_move_argument(audio_base64)
self.update()
async def get_base64_audio(self, audio_uri: str) -> str:
import sys
if sys.platform == "emscripten": # Pyodide environment
from pyodide.code import run_js
# Get the base64 representation of the blob
js_code = f"""
async function getBase64(uri) {{
const response = await fetch(uri);
const blob = await response.blob();
return new Promise((resolve, reject) => {{
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}});
}}
getBase64("{audio_uri}")
"""
base64_data = await run_js(js_code)
return base64_data
else:
# For non-Pyodide environments, assume audio_uri is already a base64 string
return await get_base64_audio(self.my_page, audio_uri)
def on_recorder_state_changed(self, e):
if e.data == "stopped":
pass # We don't need to do anything here anymore
def on_text_submit(self, e: ft.ControlEvent):
self.presenter.on_text_submit(self.text_input.value)
self.text_input.value = ""
self.update()
def switch_turn(self):
self.current_turn = not self.current_turn
self.update_turn_indicator()
def update_turn_indicator(self):
turn_text = "White's turn" if self.current_turn == chess.WHITE else "Black's turn"
self.turn_indicator.value = self.get_localized_string(turn_text)
self.update()
def reset_turn(self):
self.current_turn = chess.WHITE
self.update_turn_indicator()
def on_square_click(self, row, col):
square = chess.square(col, 7 - row)
piece, chess_piece = self.presenter.model.get_piece_at(square)
if self.elimination_mode:
self.presenter.eliminate_rival_piece(square)
return
if chess_piece and chess_piece.color == self.current_turn:
# Show empathy bar when a valid piece is clicked
print(f"Valid piece selected: {chess_piece}, empathy: {chess_piece.empathy}")
self.show_empathy_bar(chess_piece.empathy)
else:
print("No valid piece selected")
self.hide_empathy_bar()
# Delegate handling the square click to the presenter
self.presenter.on_square_click(row, col)
def show_empathy_bar(self, empathy):
print(f"Showing empathy bar with value: {empathy}")
# Ensure the empathy value is between 0 and 1
self.empathy_bar.value = max(0.0, min(empathy, 1.0))
# Make the empathy container visible
self.empathy_container.visible = True
self.update()
def hide_empathy_bar(self):
print("Hiding empathy bar")
self.empathy_container.visible = False
self.update()
def show_sprite_and_dialogue(self, piece: str):
piece_image_path = self.piece_images.get(piece.upper(), "")
self.sprite_placeholder.src = piece_image_path
# Create a stack with the sprite and dialogue overlapped
sprite_and_dialogue = ft.Stack([
self.sprite_placeholder,
ft.Container(
content=self.piece_dialogue,
alignment=ft.alignment.bottom_center,
bottom=20, # Adjust this value to position the dialogue
left=0,
right=0,
)
])
# Update the response_layout with the new content
self.response_layout.content = sprite_and_dialogue
self.response_layout.visible = True
self.update()
def hide_sprite_and_dialogue(self):
self.response_layout.visible = False
self.update()
def hide_player_transcription(self):
self.transcription_container.visible = False
self.update()
def update_piece_voice_attributes(self, piece_speak_velocity: str):
self.set_velocity(self.piece_speak_velocity_bar, piece_speak_velocity)
self.update()
def update_player_voice_attributes(self, transcription: Optional[str] = None, player_speak_velocity: Optional[str] = None):
if transcription and player_speak_velocity:
self.transcription_text.value = transcription
self.set_velocity(self.player_speak_velocity_bar, player_speak_velocity)
self.transcription_container.visible = True
else:
# Don't clear the transcription text or hide the container
self.set_velocity(self.player_speak_velocity_bar, "low") # Reset to low
self.update()
def accept_elimination(self, _=None):
self.hide_eliminate_piece_popup()
self.presenter.start_piece_elimination()
def build_ui(self):
self.checkmate_dialog = ft.AlertDialog(
modal=True,
title=ft.Text("Checkmate!"),
content=ft.Text(""),
actions=[
ft.TextButton("Restart", on_click=self.restart_game),
],
actions_alignment=ft.MainAxisAlignment.END,
)
self.king_empathy_loss_dialog = ft.AlertDialog(
modal=True,
title=ft.Text("Game Over"),
content=ft.Text(""),
actions=[
ft.TextButton("Restart", on_click=self.restart_game),
],
actions_alignment=ft.MainAxisAlignment.END,
)
self.record_button = ft.IconButton(
icon=ft.icons.MIC,
on_click=self.toggle_recording,
tooltip="Record move argument"
)
self.video_record_button = ft.IconButton(
icon=ft.icons.VIDEO_CALL,
on_click=lambda _: self.presenter.toggle_video_recording(),
tooltip="Record video move argument"
)
self.text_input = ft.TextField(
label="Enter your move description",
on_submit=self.on_text_submit,
width=200
)
self.submit_button = ft.ElevatedButton("Submit", on_click=self.on_text_submit)
self.move_argument_text = ft.Text("Type your move or use the microphone to record")
self.move_argument_dialog = ft.AlertDialog(
title=ft.Text("Describe your move"),
content=ft.Column([
self.countdown,
self.move_argument_text,
ft.Row([self.text_input]),
self.submit_button,
ft.Row([
self.record_button,
self.video_record_button,
]),
ft.Row([
self.processing_indicator,
self.processing_text
], alignment=ft.MainAxisAlignment.CENTER),
self.video_container, # Use the video_container instead of video_placeholder
], tight=True),
)
self.turn_indicator = ft.Text("White's turn", size=20, color=ft.colors.WHITE)
# Create separate containers for turn indicator and empathy bar
turn_container = ft.Container(
content=self.turn_indicator,
margin=ft.margin.only(bottom=5)
)
# Place turn container, empathy container, and chessboard in a column
board_column = ft.Column([
turn_container,
self.empathy_container,
ft.Container(ft.Stack([self.chessboard, self.move_argument_dialog]), bgcolor='blue')
], alignment=ft.MainAxisAlignment.CENTER, spacing=10) # Added spacing between elements
main_layout = ft.Container(
content=board_column,
margin=ft.margin.only(top=20)
)
full_layout = ft.Stack([
ft.Column(controls=[
ft.Container(ft.Column(controls=[main_layout])),
self.checkmate_dialog,
self.eliminate_piece_dialog,
self.king_empathy_loss_dialog,
self.transcription_container # Add this line
]),
self.response_layout, # Now part of the main stack
])
return ft.Container(
content=full_layout,
width=360,
height=800,
on_click=self.on_screen_click
)
def speak_velocity_bar(self, width=150, height=15):
segments = [
ft.Container(width=width/3, height=height, bgcolor=ft.colors.GREEN),
ft.Container(width=width/3, height=height, bgcolor=ft.colors.GREY_300),
ft.Container(width=width/3, height=height, bgcolor=ft.colors.GREY_300)
]
return ft.Row(segments, spacing=2)
def set_velocity(self, bar, velocity: str):
if velocity:
colors = {
"low": [ft.colors.GREEN, ft.colors.GREY_300, ft.colors.GREY_300],
"medium": [ft.colors.GREEN, ft.colors.YELLOW, ft.colors.GREY_300],
"high": [ft.colors.GREEN, ft.colors.YELLOW, ft.colors.RED]
}
selected_colors = colors.get(velocity.lower(), colors["low"])
for i, color in enumerate(selected_colors):
bar.controls[i].bgcolor = color
self.update()
def is_camera_active(self):
return self.camera_image.stream_active
def start_camera_stream(self):
self.my_page.run_task(self.camera_image.start_camera_stream_async)
def stop_camera_stream(self):
self.my_page.run_task(self.camera_image.stop_camera_stream_async)
def show_video_container(self):
self.video_container.visible = True
self.update()
def hide_video_container(self):
self.video_container.visible = False
self.update()
def on_screen_click(self, e):
if self.empathy_bar_visible:
print("Screen clicked, hiding empathy bar")
self.hide_empathy_bar()
self.hide_sprite_and_dialogue()
# Clear the self.transcription_text.value = ""
self.update()
async def main(page: ft.Page):
import sys
print("SYS PLATFORM!:")
print(sys.platform)
if sys.platform == "emscripten": # check if run in Pyodide environment
print("WE ARE RUNNING PYODIDE!")
from pyodide.code import run_js
# Run arbitrary JavaScript code
result = run_js('Math.random() * 100')
# The result is returned to Python
print("Random number from JS:", result)
page.title = "Multiplayer Chess"
page.window_width = 360
page.window_height = 800
page.window_resizable = True
page.padding = 0
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.vertical_alignment = ft.MainAxisAlignment.CENTER
chess_view = ChessView(page)
page.overlay.append(chess_view.audio_recorder)
page.add(chess_view)
chess_view.initialize_view() # Call this after adding to the page
page.overlay.extend([chess_view.move_sound, chess_view.capture_sound, chess_view.audio_player])
page.update() # Needed to update overlay
if __name__ == "__main__":
app = ft.app(target=main)