Improve connect 4 frontend
This commit is contained in:
250
flanabot/connect_4_frontend.py
Normal file
250
flanabot/connect_4_frontend.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import io
|
||||
import math
|
||||
import random
|
||||
from typing import Sequence
|
||||
|
||||
import cairo
|
||||
|
||||
from flanabot import constants
|
||||
from flanabot.models.player import Player
|
||||
|
||||
SIZE_MULTIPLIER = 1
|
||||
LEFT_MARGIN = 20
|
||||
TOP_MARGIN = 20
|
||||
CELL_LENGTH = 100 * SIZE_MULTIPLIER
|
||||
NUMBERS_HEIGHT = 30 * SIZE_MULTIPLIER
|
||||
TEXT_HEIGHT = 70 * SIZE_MULTIPLIER
|
||||
NUMBERS_X_INITIAL_POSITION = LEFT_MARGIN + CELL_LENGTH / 2 - 8 * SIZE_MULTIPLIER
|
||||
NUMBERS_Y_POSITION = TOP_MARGIN + CELL_LENGTH * constants.CONNECT_4_N_ROWS + 40 * SIZE_MULTIPLIER
|
||||
TEXT_POSITION = (LEFT_MARGIN, TOP_MARGIN + CELL_LENGTH * constants.CONNECT_4_N_ROWS + NUMBERS_HEIGHT + 70 * SIZE_MULTIPLIER)
|
||||
SURFACE_WIDTH = LEFT_MARGIN * 2 + CELL_LENGTH * constants.CONNECT_4_N_COLUMNS
|
||||
SURFACE_HEIGHT = TOP_MARGIN * 2 + CELL_LENGTH * constants.CONNECT_4_N_ROWS + NUMBERS_HEIGHT + TEXT_HEIGHT
|
||||
|
||||
CIRCLE_RADIUS = 36 * SIZE_MULTIPLIER
|
||||
CROSS_LINE_WIDTH = 24 * SIZE_MULTIPLIER
|
||||
FONT_SIZE = 32 * SIZE_MULTIPLIER
|
||||
TABLE_LINE_WIDTH = 4 * SIZE_MULTIPLIER
|
||||
|
||||
BLUE = (66 / 255, 135 / 255, 245 / 255)
|
||||
BACKGROUND_COLOR = (54 / 255, 57 / 255, 63 / 255)
|
||||
GRAY = (200 / 255, 200 / 255, 200 / 255)
|
||||
HIGHLIGHT_COLOR = (104 / 255, 107 / 255, 113 / 255)
|
||||
RED = (255 / 255, 70 / 255, 70 / 255)
|
||||
PLAYER_1_COLOR = BLUE
|
||||
PLAYER_2_COLOR = RED
|
||||
|
||||
|
||||
def center_point(board_position: Sequence[int]) -> tuple[float, float]:
|
||||
return LEFT_MARGIN + (board_position[1] + 0.5) * CELL_LENGTH, TOP_MARGIN + (board_position[0] + 0.5) * CELL_LENGTH
|
||||
|
||||
|
||||
def draw_circle(board_position: Sequence[int], radius: float, color: tuple[float, float, float], context: cairo.Context):
|
||||
context.set_source_rgba(*color)
|
||||
context.arc(*center_point(board_position), radius, 0, 2 * math.pi)
|
||||
context.fill()
|
||||
|
||||
|
||||
def draw_line(
|
||||
board_position_start: Sequence[int],
|
||||
board_position_end: Sequence[int],
|
||||
line_width: float,
|
||||
color: tuple[float, float, float],
|
||||
context: cairo.Context
|
||||
):
|
||||
context.set_line_width(line_width)
|
||||
context.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
context.set_source_rgba(*color)
|
||||
context.move_to(*center_point(board_position_start))
|
||||
context.line_to(*center_point(board_position_end))
|
||||
context.stroke()
|
||||
|
||||
|
||||
def draw_table(line_width: float, color: tuple[float, float, float], context: cairo.Context):
|
||||
context.set_line_width(line_width)
|
||||
context.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
context.set_source_rgba(*color)
|
||||
|
||||
x = LEFT_MARGIN
|
||||
y = TOP_MARGIN
|
||||
for _ in range(constants.CONNECT_4_N_ROWS + 1):
|
||||
context.move_to(x, y)
|
||||
context.line_to(x + CELL_LENGTH * constants.CONNECT_4_N_COLUMNS, y)
|
||||
context.stroke()
|
||||
y += CELL_LENGTH
|
||||
|
||||
x = LEFT_MARGIN
|
||||
y = TOP_MARGIN
|
||||
for _ in range(constants.CONNECT_4_N_COLUMNS + 1):
|
||||
context.move_to(x, y)
|
||||
context.line_to(x, y + CELL_LENGTH * constants.CONNECT_4_N_ROWS)
|
||||
context.stroke()
|
||||
x += CELL_LENGTH
|
||||
|
||||
|
||||
def draw_text(
|
||||
text: str,
|
||||
point: Sequence[float],
|
||||
color: tuple[float, float, float],
|
||||
font_size: float,
|
||||
italic: bool,
|
||||
context: cairo.Context
|
||||
):
|
||||
context.move_to(*point)
|
||||
context.set_source_rgba(*color)
|
||||
context.select_font_face("Sans", cairo.FONT_SLANT_ITALIC if italic else cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
|
||||
context.set_font_size(font_size)
|
||||
context.show_text(text)
|
||||
|
||||
|
||||
def draw_winner_lines(
|
||||
win_position: Sequence[int],
|
||||
board: list[list[int | None]],
|
||||
color: tuple[float, float, float],
|
||||
context: cairo.Context
|
||||
):
|
||||
i, j = win_position
|
||||
player_number = board[i][j]
|
||||
|
||||
# horizontal
|
||||
j_a = j - 1
|
||||
while j_a >= 0 and board[i][j_a] == player_number:
|
||||
j_a -= 1
|
||||
j_b = j + 1
|
||||
while j_b < constants.CONNECT_4_N_COLUMNS and board[i][j_b] == player_number:
|
||||
j_b += 1
|
||||
if abs(j_a - j) + abs(j_b - j) - 1 >= 4:
|
||||
draw_line((i, j_a + 1), (i, j_b - 1), CROSS_LINE_WIDTH, color, context)
|
||||
|
||||
# vertical
|
||||
i_a = i - 1
|
||||
while i_a >= 0 and board[i_a][j] == player_number:
|
||||
i_a -= 1
|
||||
i_b = i + 1
|
||||
while i_b < constants.CONNECT_4_N_ROWS and board[i_b][j] == player_number:
|
||||
i_b += 1
|
||||
if abs(i_a - i) + abs(i_b - i) - 1 >= 4:
|
||||
draw_line((i_a + 1, j), (i_b - 1, j), CROSS_LINE_WIDTH, color, context)
|
||||
|
||||
# diagonal 1
|
||||
i_a = i - 1
|
||||
j_a = j - 1
|
||||
while i_a >= 0 and j_a >= 0 and board[i_a][j_a] == player_number:
|
||||
i_a -= 1
|
||||
j_a -= 1
|
||||
i_b = i + 1
|
||||
j_b = j + 1
|
||||
while i_b < constants.CONNECT_4_N_ROWS and j_b < constants.CONNECT_4_N_COLUMNS and board[i_b][j_b] == player_number:
|
||||
i_b += 1
|
||||
j_b += 1
|
||||
if abs(i_a - i) + abs(i_b - i) - 1 >= 4:
|
||||
draw_line((i_a + 1, j_a + 1), (i_b - 1, j_b - 1), CROSS_LINE_WIDTH, color, context)
|
||||
|
||||
# diagonal 2
|
||||
i_a = i - 1
|
||||
j_a = j + 1
|
||||
while i_a >= 0 and j_a < constants.CONNECT_4_N_COLUMNS and board[i_a][j_a] == player_number:
|
||||
i_a -= 1
|
||||
j_a += 1
|
||||
i_b = i + 1
|
||||
j_b = j - 1
|
||||
while i_b < constants.CONNECT_4_N_ROWS and j_b >= 0 and board[i_b][j_b] == player_number:
|
||||
i_b += 1
|
||||
j_b -= 1
|
||||
if abs(i_a - i) + abs(i_b - i) - 1 >= 4:
|
||||
draw_line((i_a + 1, j_a - 1), (i_b - 1, j_b + 1), CROSS_LINE_WIDTH, color, context)
|
||||
|
||||
|
||||
def highlight_cell(
|
||||
board_position: Sequence[int],
|
||||
color: tuple[float, float, float],
|
||||
context: cairo.Context
|
||||
):
|
||||
x, y = top_left_point(board_position)
|
||||
context.move_to(x, y)
|
||||
x += CELL_LENGTH
|
||||
context.line_to(x, y)
|
||||
y += CELL_LENGTH
|
||||
context.line_to(x, y)
|
||||
x -= CELL_LENGTH
|
||||
context.line_to(x, y)
|
||||
context.close_path()
|
||||
context.set_source_rgba(*color)
|
||||
context.fill()
|
||||
|
||||
|
||||
def make_image(
|
||||
board: list[list[int | None]],
|
||||
player: Player = None,
|
||||
highlight=None,
|
||||
win_position: Sequence[int] = None,
|
||||
tie=False
|
||||
) -> bytes:
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, SURFACE_WIDTH, SURFACE_HEIGHT)
|
||||
context = cairo.Context(surface)
|
||||
|
||||
paint_background(BACKGROUND_COLOR, context)
|
||||
if highlight:
|
||||
highlight_cell(highlight, HIGHLIGHT_COLOR, context)
|
||||
draw_table(TABLE_LINE_WIDTH, GRAY, context)
|
||||
write_numbers(GRAY, context)
|
||||
for i in range(constants.CONNECT_4_N_ROWS):
|
||||
for j in range(constants.CONNECT_4_N_COLUMNS):
|
||||
match board[i][j]:
|
||||
case 1:
|
||||
draw_circle((i, j), CIRCLE_RADIUS, PLAYER_1_COLOR, context)
|
||||
case 2:
|
||||
draw_circle((i, j), CIRCLE_RADIUS, PLAYER_2_COLOR, context)
|
||||
|
||||
if tie:
|
||||
write_tie(context)
|
||||
elif win_position:
|
||||
player_color = PLAYER_1_COLOR if player.number == 1 else PLAYER_2_COLOR
|
||||
draw_winner_lines(win_position, board, player_color, context)
|
||||
write_winner(player.name, player_color, context)
|
||||
else:
|
||||
write_player_turn(player.name, PLAYER_1_COLOR if player.number == 1 else PLAYER_2_COLOR, context)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
surface.write_to_png(buffer)
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def paint_background(color: tuple[float, float, float], context: cairo.Context):
|
||||
context.set_source_rgba(*color)
|
||||
context.paint()
|
||||
|
||||
|
||||
def top_left_point(board_position: Sequence[int]) -> tuple[float, float]:
|
||||
return LEFT_MARGIN + board_position[1] * CELL_LENGTH, TOP_MARGIN + board_position[0] * CELL_LENGTH
|
||||
|
||||
|
||||
def write_numbers(color: tuple[float, float, float], context: cairo.Context):
|
||||
x = NUMBERS_X_INITIAL_POSITION
|
||||
for j in range(constants.CONNECT_4_N_COLUMNS):
|
||||
draw_text(str(j + 1), (x, NUMBERS_Y_POSITION), color, FONT_SIZE, italic=False, context=context)
|
||||
x += CELL_LENGTH
|
||||
|
||||
|
||||
def write_player_turn(name: str, color: tuple[float, float, float], context: cairo.Context):
|
||||
point = TEXT_POSITION
|
||||
text = 'Turno de '
|
||||
draw_text(text, point, GRAY, FONT_SIZE, True, context)
|
||||
point = (point[0] + context.text_extents(text).width + 9 * SIZE_MULTIPLIER, point[1])
|
||||
draw_text(name, point, color, FONT_SIZE, True, context)
|
||||
point = (point[0] + context.text_extents(name).width, point[1])
|
||||
draw_text('.', point, GRAY, FONT_SIZE, True, context)
|
||||
|
||||
|
||||
def write_tie(context: cairo.Context):
|
||||
draw_text(f"Empate{random.choice(('.', ' :c', ' :/', ' :s'))}", TEXT_POSITION, GRAY, FONT_SIZE, True, context)
|
||||
|
||||
|
||||
def write_winner(name: str, color: tuple[float, float, float], context: cairo.Context):
|
||||
point = TEXT_POSITION
|
||||
text = 'Ha ganado '
|
||||
draw_text(text, TEXT_POSITION, GRAY, FONT_SIZE, True, context)
|
||||
point = (point[0] + context.text_extents(text).width + 6 * SIZE_MULTIPLIER, point[1])
|
||||
draw_text(name, point, color, FONT_SIZE, True, context)
|
||||
point = (point[0] + context.text_extents(name).width, point[1])
|
||||
draw_text('!!!', point, GRAY, FONT_SIZE, True, context)
|
||||
12
flanabot/models/player.py
Normal file
12
flanabot/models/player.py
Normal file
@@ -0,0 +1,12 @@
|
||||
__all__ = ['Player']
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from flanautils import FlanaBase
|
||||
|
||||
|
||||
@dataclass
|
||||
class Player(FlanaBase):
|
||||
id: int
|
||||
name: str
|
||||
number: int
|
||||
Reference in New Issue
Block a user