Files
flanabot/flanabot/connect_4_frontend.py
2023-04-07 06:22:22 +02:00

262 lines
8.7 KiB
Python

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 = (49 / 255, 51 / 255, 56 / 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]],
next_turn_player: Player = None,
winner: Player = None,
loser: 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 winner:
player_color = PLAYER_1_COLOR if winner.number == 1 else PLAYER_2_COLOR
draw_winner_lines(win_position, board, player_color, context)
write_winner(winner, loser, context)
else:
write_player_turn(next_turn_player.name, PLAYER_1_COLOR if next_turn_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):
text = 'Turno de '
point = TEXT_POSITION
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(winner: Player, loser: Player, context: cairo.Context):
winner_color, loser_color = (PLAYER_1_COLOR, PLAYER_2_COLOR) if winner.number == 1 else (PLAYER_2_COLOR, PLAYER_1_COLOR)
point = TEXT_POSITION
draw_text(winner.name, point, winner_color, FONT_SIZE, True, context)
text = ' le ha ganado a '
point = (point[0] + context.text_extents(winner.name).width + 3 * SIZE_MULTIPLIER, point[1])
draw_text(text, point, GRAY, FONT_SIZE, True, context)
point = (point[0] + context.text_extents(text).width + 10 * SIZE_MULTIPLIER, point[1])
draw_text(loser.name, point, loser_color, FONT_SIZE, True, context)
point = (point[0] + context.text_extents(loser.name).width + 3 * SIZE_MULTIPLIER, point[1])
draw_text('!!!', point, GRAY, FONT_SIZE, True, context)