|
import gradio as gr |
|
import numpy as np |
|
import random |
|
from PIL import Image, ImageDraw, ImageFont |
|
import io |
|
import base64 |
|
|
|
class LudoGame: |
|
def __init__(self): |
|
|
|
self.colors = ["red", "green", "yellow", "blue"] |
|
self.color_codes = { |
|
"red": "#FF5555", |
|
"green": "#55FF55", |
|
"yellow": "#FFFF55", |
|
"blue": "#5555FF", |
|
"white": "#FFFFFF", |
|
"black": "#000000" |
|
} |
|
|
|
|
|
self.reset_game() |
|
|
|
def reset_game(self): |
|
|
|
self.current_player = 0 |
|
self.dice_value = 1 |
|
self.dice_rolled = False |
|
self.winner = None |
|
|
|
|
|
self.tokens = {} |
|
for i, color in enumerate(self.colors): |
|
self.tokens[color] = [-1, -1, -1, -1] |
|
|
|
|
|
self.can_play = False |
|
|
|
|
|
self.message = f"Game started! {self.colors[self.current_player].capitalize()}'s turn to roll." |
|
|
|
def roll_dice(self): |
|
"""Roll the dice and return the value""" |
|
if self.winner: |
|
return self.render_board() |
|
|
|
if self.dice_rolled: |
|
self.message = f"You already rolled a {self.dice_value}. Please move a token or pass." |
|
return self.render_board() |
|
|
|
self.dice_value = random.randint(1, 6) |
|
self.dice_rolled = True |
|
|
|
|
|
self.can_play = self._can_play() |
|
|
|
if not self.can_play: |
|
|
|
if self.dice_value == 6: |
|
self.message = f"{self.colors[self.current_player].capitalize()} rolled a 6 but can't move. Roll again!" |
|
self.dice_rolled = False |
|
else: |
|
self.message = f"{self.colors[self.current_player].capitalize()} rolled {self.dice_value} but can't move. Next player's turn." |
|
self._next_player() |
|
else: |
|
self.message = f"{self.colors[self.current_player].capitalize()} rolled {self.dice_value}. Choose a token to move." |
|
|
|
return self.render_board() |
|
|
|
def _can_play(self): |
|
"""Check if current player can move any token""" |
|
current_color = self.colors[self.current_player] |
|
tokens = self.tokens[current_color] |
|
|
|
for i, position in enumerate(tokens): |
|
if position == -1 and self.dice_value == 6: |
|
|
|
return True |
|
elif position >= 0: |
|
|
|
return True |
|
|
|
return False |
|
|
|
def move_token(self, token_idx): |
|
"""Move the selected token for the current player""" |
|
if self.winner: |
|
return self.render_board() |
|
|
|
if not self.dice_rolled: |
|
self.message = "Please roll the dice first." |
|
return self.render_board() |
|
|
|
if not self.can_play: |
|
self.message = "You can't move any token. Please pass your turn." |
|
return self.render_board() |
|
|
|
current_color = self.colors[self.current_player] |
|
current_pos = self.tokens[current_color][token_idx] |
|
|
|
|
|
if current_pos == -1 and self.dice_value != 6: |
|
self.message = "Need to roll a 6 to move a token out of home." |
|
return self.render_board() |
|
|
|
|
|
if current_pos == -1 and self.dice_value == 6: |
|
|
|
self.tokens[current_color][token_idx] = self.current_player * 13 |
|
self.message = f"{current_color.capitalize()} token {token_idx+1} is now on the board." |
|
self.dice_rolled = False |
|
return self.render_board() |
|
|
|
|
|
new_pos = (current_pos + self.dice_value) % 52 |
|
|
|
|
|
for color in self.colors: |
|
if color != current_color: |
|
for i, pos in enumerate(self.tokens[color]): |
|
if pos == new_pos: |
|
|
|
self.tokens[color][i] = -1 |
|
self.message = f"{current_color.capitalize()} captured {color}'s token!" |
|
|
|
|
|
self.tokens[current_color][token_idx] = new_pos |
|
|
|
|
|
if self._check_winner(): |
|
self.winner = self.current_player |
|
self.message = f"{current_color.capitalize()} wins the game!" |
|
else: |
|
self.message = f"{current_color.capitalize()} moved token {token_idx+1} to position {new_pos}." |
|
|
|
|
|
if self.dice_value == 6: |
|
self.message += " Roll again!" |
|
else: |
|
self._next_player() |
|
|
|
self.dice_rolled = False |
|
return self.render_board() |
|
|
|
def _check_winner(self): |
|
"""Very simple win check - if all tokens made a complete circuit""" |
|
current_color = self.colors[self.current_player] |
|
starting_pos = self.current_player * 13 |
|
|
|
for pos in self.tokens[current_color]: |
|
if pos < starting_pos: |
|
return False |
|
|
|
return True |
|
|
|
def _next_player(self): |
|
"""Move to the next player's turn""" |
|
self.current_player = (self.current_player + 1) % 4 |
|
self.dice_rolled = False |
|
self.message += f" {self.colors[self.current_player].capitalize()}'s turn to roll." |
|
|
|
def pass_turn(self): |
|
"""Pass the current player's turn""" |
|
if self.winner: |
|
return self.render_board() |
|
|
|
if not self.dice_rolled: |
|
self.message = "Please roll the dice first." |
|
return self.render_board() |
|
|
|
if self.can_play: |
|
self.message = "You have valid moves available. Please move a token." |
|
return self.render_board() |
|
|
|
self._next_player() |
|
return self.render_board() |
|
|
|
def render_board(self): |
|
"""Render the Ludo board as an image""" |
|
|
|
width, height = 600, 600 |
|
board = Image.new('RGB', (width, height), color='white') |
|
draw = ImageDraw.Draw(board) |
|
|
|
|
|
|
|
draw.rectangle([(50, 50), (550, 550)], outline='black', width=2) |
|
|
|
|
|
home_squares = [ |
|
(50, 50, 250, 250), |
|
(350, 50, 550, 250), |
|
(50, 350, 250, 550), |
|
(350, 350, 550, 550) |
|
] |
|
|
|
for i, color in enumerate(self.colors): |
|
draw.rectangle(home_squares[i], fill=self.color_codes[color], outline='black', width=2) |
|
|
|
|
|
draw.rectangle([(250, 250), (350, 350)], fill='white', outline='black', width=2) |
|
|
|
|
|
|
|
draw.rectangle([(250, 50), (350, 250)], fill='white', outline='black', width=1) |
|
draw.rectangle([(250, 350), (350, 550)], fill='white', outline='black', width=1) |
|
draw.rectangle([(50, 250), (250, 350)], fill='white', outline='black', width=1) |
|
draw.rectangle([(350, 250), (550, 350)], fill='white', outline='black', width=1) |
|
|
|
|
|
for color_idx, color in enumerate(self.colors): |
|
for token_idx, position in enumerate(self.tokens[color]): |
|
if position == -1: |
|
|
|
home_x = home_squares[color_idx][0] + 50 + (token_idx % 2) * 100 |
|
home_y = home_squares[color_idx][1] + 50 + (token_idx // 2) * 100 |
|
draw.ellipse([(home_x-20, home_y-20), (home_x+20, home_y+20)], |
|
fill=self.color_codes[color], outline='black', width=2) |
|
else: |
|
|
|
|
|
board_positions = [ |
|
|
|
(100, 300), (150, 300), (200, 300), (250, 300), (300, 300), (350, 300), (400, 300), (450, 300), (500, 300), |
|
|
|
(500, 350), (500, 400), (500, 450), (500, 500), |
|
|
|
(450, 500), (400, 500), (350, 500), (300, 500), (250, 500), (200, 500), (150, 500), (100, 500), |
|
|
|
(100, 450), (100, 400), (100, 350), (100, 300), |
|
|
|
|
|
(100, 300), (150, 300), (200, 300), (250, 300), (300, 300), (350, 300), (400, 300), (450, 300), (500, 300), |
|
(500, 350), (500, 400), (500, 450), (500, 500), |
|
(450, 500), (400, 500), (350, 500), (300, 500), (250, 500), (200, 500), (150, 500), (100, 500), |
|
(100, 450), (100, 400), (100, 350), (100, 300), |
|
] |
|
|
|
if position < len(board_positions): |
|
token_x, token_y = board_positions[position] |
|
draw.ellipse([(token_x-15, token_y-15), (token_x+15, token_y+15)], |
|
fill=self.color_codes[color], outline='black', width=2) |
|
|
|
draw.text((token_x-5, token_y-5), str(token_idx+1), fill='black') |
|
|
|
|
|
dice_x, dice_y = 300, 300 |
|
draw.rectangle([(dice_x-25, dice_y-25), (dice_x+25, dice_y+25)], fill='white', outline='black', width=2) |
|
|
|
|
|
if self.dice_value == 1: |
|
draw.ellipse([(dice_x-5, dice_y-5), (dice_x+5, dice_y+5)], fill='black') |
|
elif self.dice_value == 2: |
|
draw.ellipse([(dice_x-15, dice_y-15), (dice_x-5, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y+5), (dice_x+15, dice_y+15)], fill='black') |
|
elif self.dice_value == 3: |
|
draw.ellipse([(dice_x-15, dice_y-15), (dice_x-5, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x-5, dice_y-5), (dice_x+5, dice_y+5)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y+5), (dice_x+15, dice_y+15)], fill='black') |
|
elif self.dice_value == 4: |
|
draw.ellipse([(dice_x-15, dice_y-15), (dice_x-5, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y-15), (dice_x+15, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x-15, dice_y+5), (dice_x-5, dice_y+15)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y+5), (dice_x+15, dice_y+15)], fill='black') |
|
elif self.dice_value == 5: |
|
draw.ellipse([(dice_x-15, dice_y-15), (dice_x-5, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y-15), (dice_x+15, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x-5, dice_y-5), (dice_x+5, dice_y+5)], fill='black') |
|
draw.ellipse([(dice_x-15, dice_y+5), (dice_x-5, dice_y+15)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y+5), (dice_x+15, dice_y+15)], fill='black') |
|
elif self.dice_value == 6: |
|
draw.ellipse([(dice_x-15, dice_y-15), (dice_x-5, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y-15), (dice_x+15, dice_y-5)], fill='black') |
|
draw.ellipse([(dice_x-15, dice_y-5), (dice_x-5, dice_y+5)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y-5), (dice_x+15, dice_y+5)], fill='black') |
|
draw.ellipse([(dice_x-15, dice_y+5), (dice_x-5, dice_y+15)], fill='black') |
|
draw.ellipse([(dice_x+5, dice_y+5), (dice_x+15, dice_y+15)], fill='black') |
|
|
|
|
|
current_color = self.colors[self.current_player] |
|
draw.rectangle([(20, 20), (40, 40)], fill=self.color_codes[current_color], outline='black', width=2) |
|
|
|
|
|
draw.text((50, 20), self.message, fill='black') |
|
|
|
|
|
img_byte_arr = io.BytesIO() |
|
board.save(img_byte_arr, format='PNG') |
|
img_byte_arr.seek(0) |
|
|
|
return img_byte_arr |
|
|
|
|
|
def create_ludo_game(): |
|
game = LudoGame() |
|
|
|
def roll(): |
|
return game.roll_dice() |
|
|
|
def move_token_0(): |
|
return game.move_token(0) |
|
|
|
def move_token_1(): |
|
return game.move_token(1) |
|
|
|
def move_token_2(): |
|
return game.move_token(2) |
|
|
|
def move_token_3(): |
|
return game.move_token(3) |
|
|
|
def pass_turn(): |
|
return game.pass_turn() |
|
|
|
def reset(): |
|
game.reset_game() |
|
return game.render_board() |
|
|
|
with gr.Blocks() as ludo_app: |
|
gr.Markdown("# Ludo Game") |
|
gr.Markdown("### A classic 4-player board game") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
image_output = gr.Image(type="pil", label="Ludo Board") |
|
|
|
with gr.Row(): |
|
roll_button = gr.Button("Roll Dice") |
|
|
|
with gr.Row(): |
|
token1_button = gr.Button("Move Token 1") |
|
token2_button = gr.Button("Move Token 2") |
|
token3_button = gr.Button("Move Token 3") |
|
token4_button = gr.Button("Move Token 4") |
|
|
|
with gr.Row(): |
|
pass_button = gr.Button("Pass Turn") |
|
reset_button = gr.Button("Reset Game") |
|
|
|
|
|
roll_button.click(roll, inputs=[], outputs=[image_output]) |
|
token1_button.click(move_token_0, inputs=[], outputs=[image_output]) |
|
token2_button.click(move_token_1, inputs=[], outputs=[image_output]) |
|
token3_button.click(move_token_2, inputs=[], outputs=[image_output]) |
|
token4_button.click(move_token_3, inputs=[], outputs=[image_output]) |
|
pass_button.click(pass_turn, inputs=[], outputs=[image_output]) |
|
reset_button.click(reset, inputs=[], outputs=[image_output]) |
|
|
|
|
|
ludo_app.load(fn=game.render_board, inputs=None, outputs=image_output) |
|
|
|
return ludo_app |
|
|
|
|
|
if __name__ == "__main__": |
|
app = create_ludo_game() |
|
app.launch() |