]> git.mar77i.info Git - zenbook_gui/commitdiff
add connect_four.py
authormar77i <mar77i@protonmail.ch>
Fri, 14 Feb 2025 09:04:17 +0000 (10:04 +0100)
committermar77i <mar77i@protonmail.ch>
Fri, 14 Feb 2025 09:04:17 +0000 (10:04 +0100)
connect_four.py [new file with mode: 0755]

diff --git a/connect_four.py b/connect_four.py
new file mode 100755 (executable)
index 0000000..afd7248
--- /dev/null
@@ -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()