--- /dev/null
+#!/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()