From: mar77i Date: Fri, 14 Feb 2025 09:04:17 +0000 (+0100) Subject: add connect_four.py X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=7e1252799321ca1dcb284c94486510149bb089cd;p=zenbook_gui add connect_four.py --- diff --git a/connect_four.py b/connect_four.py new file mode 100755 index 0000000..afd7248 --- /dev/null +++ b/connect_four.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 + +from functools import partial +from math import pi, sqrt +from time import time + +import pygame + +from ui import Button, MessageBox, Rect, Root +from vectors import StrokeCircleSegment + + +def distance(a, b): + return sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) + + +class WinMessage(MessageBox): + root: "ConnectFour" + + def __init__(self, parent, rect): + super().__init__(parent, rect, "") + assert isinstance(self.children.pop(0), Rect) + + def deactivate(self): + super().deactivate() + for row in self.root.field: + for x in range(len(row)): + if row[x]: + row[x] = 0 + self.root.top_row = self.root.FIELD_SIZE[1] + + +class ColorButton(Button): + def __init__(self, parent, rect, value, color, callback, highlight=False): + super().__init__(parent, rect, value, callback, highlight) + self.color = color + + def draw(self): + if not self.pushed: + value_color = "lime" if self.highlight else "gray" + colors = (self.color, value_color, value_color) + else: + colors = ("darkgray", "lightgray", "black") + pygame.draw.rect(self.surf, colors[0], self.rect) + pygame.draw.rect(self.surf, colors[1], self.rect, 8) + self.draw_value(colors[2]) + + +class ConnectFour(Root): + FIELD_SIZE = (7, 6) + FRAME_COLOR = "blue" + PLAYER_COLORS = ("red", "green") + RADIUS = 0.45 + PULL_DOWN = 192 + PULL_TIMEOUT = 1 + + def __init__(self): + pygame.init() + super().__init__( + pygame.display.set_mode((0, 0), pygame.FULLSCREEN, display=0), + pygame.font.Font(None, size=96), + ) + size = self.surf.get_size() + self.rect = pygame.Rect( + (size[0] // 4, size[1] // 3), + (size[0] // 2, size[1] // 2), + ) + self.part_size = ( + self.rect.width // self.FIELD_SIZE[0], + self.rect.height // self.FIELD_SIZE[1], + ) + self.outer_rect = pygame.Rect( + (self.rect.left, self.rect.top - self.part_size[1]), + (self.rect.width, self.rect.height + self.part_size[1]), + ) + self.part_radius = int(self.RADIUS * min(self.part_size)) + self.field = [ + [0] * self.FIELD_SIZE[0] for _ in range(self.FIELD_SIZE[1]) + ] + self.adding = None + self.drop_y = 0 + self.current_player = 0 + self.message_box = WinMessage( + self, + pygame.Rect( + (size[0] // 6, size[1] * 3 // 8), (size[0] * 2 // 3, size[1] // 4) + ) + ) + self.top_button = ColorButton( + self, + pygame.Rect((size[0] // 2 - 256, size[1] // 6 - 256), (512, 128)), + "Change Color", + self.PLAYER_COLORS[self.current_player], + self.change_player, + ) + self.top_row = self.FIELD_SIZE[1] + + def get_field_x(self, x): + x = (x - self.rect.left) // self.part_size[0] + if x == self.FIELD_SIZE[0]: + return self.FIELD_SIZE[0] - 1 + return x + + def draw_row_above(self): + left = self.outer_rect.left + self.part_size[0] // 2 + top = self.outer_rect.top + self.part_size[1] // 2 + for x in range(self.FIELD_SIZE[0]): + pygame.draw.circle( + self.surf, + "gray", + (left + x * self.part_size[0], top), + self.part_radius, + 1, + ) + + def draw_indicator(self, x, drop_y=0): + pygame.draw.circle( + self.surf, + self.PLAYER_COLORS[self.current_player], + ( + self.rect.left + self.part_size[0] * (x + .5), + self.rect.top - self.part_size[1] // 2 + drop_y, + ), + self.part_radius, + ) + + def get_frame(self): + surf = pygame.Surface(self.rect.size, pygame.SRCALPHA, 32) + surf.fill(self.FRAME_COLOR) + for y in range(self.FIELD_SIZE[1]): + for x in range(self.FIELD_SIZE[0]): + part_rect = pygame.Rect( + (x * self.part_size[0], y * self.part_size[1]), self.part_size + ) + pygame.draw.circle( + surf, + ( + pygame.Color(0, 0, 0, 0), *self.PLAYER_COLORS + )[self.field[y][x]], + part_rect.center, + self.part_radius, + ) + return surf + + def draw_adding(self): + StrokeCircleSegment( + self.adding[1:], self.PULL_DOWN, 0, pi, 16 + ).draw(self.surf, "darkgray") + + def draw(self): + self.surf.fill("black") + self.draw_row_above() + if self.adding is not None: + self.draw_indicator(self.get_field_x(self.adding[1]), self.drop_y) + self.surf.blit(self.get_frame(), self.rect.topleft) + if self.adding is not None: + self.draw_adding() + else: + pos = pygame.mouse.get_pos() + if self.outer_rect.collidepoint(pos): + self.draw_indicator(self.get_field_x(pos[0])) + super().draw() + + def handle_mousebuttondown(self, ev): + if ev.button != 1: + return + if not self.outer_rect.collidepoint(ev.pos): + return + if self.field[0][self.get_field_x(ev.pos[0])] != 0: + print("heh?") + return + self.adding = (time(), *ev.pos) + self.drop_y = 0 + self.dirty = True + + def release(self): + self.adding = None + self.dirty = True + + def handle_mousemotion(self, ev): + if self.adding is None: + if not self.outer_rect.collidepoint(ev.pos): + self.dirty = True + elif ev.rel[0] != 0: + self.dirty = True + return + if ( + ev.pos[1] < self.adding[2] + or distance(ev.pos, self.adding[1:]) > self.PULL_DOWN * 2 + ): + self.release() + return + drop_y = distance(ev.pos, self.adding[1:]) + if drop_y > self.PULL_DOWN: + x = self.get_field_x(self.adding[1]) + self.release() + self.drop(x) + return + if drop_y != self.drop_y: + self.drop_y = drop_y + self.dirty = True + + def next_player(self): + self.current_player = (self.current_player + 1) % 2 + + def drop(self, x): + y = self.FIELD_SIZE[1] - 1 + while self.field[y][x] != 0: + y -= 1 + self.field[y][x] = self.current_player + 1 + if y < self.top_row: + self.top_row = y + self.change_top_button( + "Undo", partial(self.undo, x), self.PLAYER_COLORS[self.current_player] + ) + self.next_player() + has_won = self.has_won() + if has_won is not False: + self.game_won(has_won) + + def has_won_at(self, x, y): + row = self.field[y] + v = row[x] + if v == 0: + return False + right, bottom = self.FIELD_SIZE[0] - 3, self.FIELD_SIZE[1] - 3 + a = b = c = d = 1 + for j in range(1, 4): + x_lt_right = x < right + if x_lt_right: + a += row[x + j] == v + if y < bottom: + b += self.field[y + j][x] == v + if x_lt_right: + c += self.field[y + j][x + j] == v + if x > 2: + d += self.field[y + j][x - j] == v + return 4 in (a, b, c, d) + + def has_won(self): + if 0 not in self.field[0]: + return None + for y, row in zip( + range(self.top_row, self.FIELD_SIZE[1]), self.field[self.top_row:] + ): + for x, v in enumerate(row): + if self.has_won_at(x, y): + return True + return False + + def game_won(self, outcome): + if outcome is None: + self.message_box.message = "DRAW!" + else: + self.message_box.message = "WINNER!" + self.message_box.activate() + self.change_top_button( + "Change Color", self.change_player, self.PLAYER_COLORS[self.current_player] + ) + + def handle_mousebuttonup(self, ev): + self.release() + + def update(self): + if self.adding is not None and time() - self.adding[0] >= self.PULL_TIMEOUT: + self.release() + + def change_player(self): + self.next_player() + self.top_button.color = self.PLAYER_COLORS[self.current_player] + self.dirty = True + + def change_top_button(self, text, callback, color): + self.top_button.value = text + self.top_button.callback = callback + self.top_button.color = color + + def undo(self, x): + for y in range(self.top_row, self.FIELD_SIZE[1]): + if self.field[y][x] != 0: + self.field[y][x] = 0 + self.change_top_button("Undo", lambda: None, "gray") + self.next_player() + break + + +if __name__ == "__main__": + ConnectFour().run()