-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
922 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,3 +121,8 @@ dmypy.json | |
|
||
# Pyre type checker | ||
.pyre/ | ||
|
||
# JetBrains | ||
.idea/ | ||
notebooks/ | ||
.backup/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,16 @@ | ||
# voronoi_hex | ||
Hex on a Voronoi board. | ||
|
||
Hex on a Voronoi board. | ||
|
||
This is an implementation of the board game Hex played on a randomly generated Voronoi diagram which is intended to accompany a [blog post](https://nasosev.github.io/topology/2019/07/14/voronoi_hex/) on my homepage. | ||
|
||
![Screenshot.](screenshot.png) | ||
|
||
## Controls | ||
|
||
- left-click to claim territory. | ||
|
||
## License note | ||
|
||
- The [authors](https://github.com/kb1dds/simplicialHomology) of `simplicialHomology.py` do not permit commercial usage of their code. | ||
- The version of that file included here has been ported to Python 3 (with its authors' permission). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
from typing import Tuple | ||
|
||
import numpy as np | ||
import scipy as sp | ||
from pyrsistent import freeze, pset, s, v | ||
from pyrsistent.typing import PSet | ||
from toolz.curried import map, partial, pipe | ||
|
||
from my_types import Complex, Cycle | ||
from settings import BOARD_SIZE, BORDER_SCALE, EPSILON, SEED | ||
from topology import ( | ||
closure, | ||
edges_from_cycles, | ||
faces_from_edges, | ||
outer_edges_from_cycle, | ||
zero_cells_from_one_cells, | ||
) | ||
|
||
|
||
class Board: | ||
def __init__(self) -> None: | ||
if SEED is not None: | ||
np.random.seed(SEED) | ||
|
||
def reflect(u: np.ndarray, a: np.ndarray, c: float) -> np.ndarray: | ||
return u - 2 * np.broadcast_to(a, u.shape) * ( | ||
np.reshape(np.dot(u, a) - c, (len(u), 1)) | ||
) | ||
|
||
control_points = np.random.rand(BOARD_SIZE, 2) - 0.5 | ||
reflect_control_points = partial(reflect, control_points) | ||
|
||
down_reflect = reflect_control_points(np.array([0, 1]), -0.5) | ||
up_reflect = reflect_control_points(np.array([0, 1]), 0.5) | ||
left_reflect = reflect_control_points(np.array([1, 0]), -0.5) | ||
right_reflect = reflect_control_points(np.array([1, 0]), 0.5) | ||
|
||
extended_points = np.concatenate( | ||
(control_points, up_reflect, down_reflect, left_reflect, right_reflect) | ||
) | ||
|
||
voronoi = sp.spatial.Voronoi(extended_points) | ||
|
||
self.cycles = freeze( | ||
np.array(voronoi.regions)[voronoi.point_region[: voronoi.npoints // 5]] | ||
) | ||
|
||
edges = edges_from_cycles(self.cycles) | ||
verts = zero_cells_from_one_cells(edges) | ||
|
||
self.points, self.blue_base, self.red_base, self.blue_base_cs, self.red_base_cs = self.make_border( | ||
voronoi.vertices, edges | ||
) | ||
|
||
self.xs = verts | edges | self.blue_base | self.red_base | ||
|
||
@staticmethod | ||
def make_border( | ||
vertices: np.ndarray, one_cells: Complex | ||
) -> Tuple[np.ndarray, Complex, Complex, PSet[Cycle], PSet[Cycle]]: | ||
def first_index(array: np.ndarray, value: np.ndarray) -> float: | ||
return next( | ||
i for i, _ in enumerate(array) if np.linalg.norm(value - _) < EPSILON | ||
) | ||
|
||
first_index_vertices = partial(first_index, vertices) | ||
|
||
corners = v(v(-0.5, 0.5), v(-0.5, -0.5), v(0.5, -0.5), v(0.5, 0.5)) | ||
|
||
ul, dl, dr, ur = pipe(corners, map(np.array), map(first_index_vertices)) | ||
|
||
max_ind = len(vertices) | ||
|
||
cul = max_ind | ||
cdl = max_ind + 1 | ||
cdr = max_ind + 2 | ||
cur = max_ind + 3 | ||
|
||
left_c = v(ul, cul, cdl, dl) | ||
right_c = v(dr, cdr, cur, ur) | ||
down_c = v(dl, cdl, cdr, dr) | ||
up_c = v(ur, cur, cul, ul) | ||
|
||
red_base_cs = s(left_c, right_c) | ||
blue_base_cs = s(up_c, down_c) | ||
|
||
def border_edges(vs: np.ndarray, es: Complex, pos: int, side: float) -> Complex: | ||
return pset( | ||
edge | ||
for edge in es | ||
if all( | ||
np.linalg.norm(vs[point][pos] - side) < EPSILON for point in edge | ||
) | ||
) | ||
|
||
border_edges_from_square_side = partial(border_edges, vertices, one_cells) | ||
|
||
left_faces = faces_from_edges( | ||
border_edges_from_square_side(0, -0.5) | outer_edges_from_cycle(left_c) | ||
) | ||
right_faces = faces_from_edges( | ||
border_edges_from_square_side(0, 0.5) | outer_edges_from_cycle(right_c) | ||
) | ||
down_faces = faces_from_edges( | ||
border_edges_from_square_side(1, -0.5) | outer_edges_from_cycle(down_c) | ||
) | ||
up_faces = faces_from_edges( | ||
border_edges_from_square_side(1, 0.5) | outer_edges_from_cycle(up_c) | ||
) | ||
|
||
red_base = closure(left_faces | right_faces) | ||
blue_base = closure(down_faces | up_faces) | ||
|
||
border_points = np.array(corners) * BORDER_SCALE | ||
aug_points = np.concatenate((vertices, border_points)) | ||
|
||
return aug_points, blue_base, red_base, blue_base_cs, red_base_cs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from enum import Enum | ||
from itertools import cycle | ||
|
||
from pyrsistent import s | ||
from toolz.curried import pipe | ||
|
||
from board import Board | ||
from my_types import Cycle | ||
from topology import betti, closure, face_from_cycle | ||
|
||
|
||
class Player(Enum): | ||
NEUTRAL = 1 | ||
BLUE = 2 | ||
RED = 3 | ||
|
||
|
||
class Game: | ||
def __init__(self, board: Board): | ||
self.board = board | ||
self.blue = board.blue_base | ||
self.red = board.red_base | ||
self.__player = cycle(s(Player.RED, Player.BLUE)) | ||
|
||
def player(self) -> Player: | ||
return next(self.__player) | ||
|
||
def add_cycle_to_player_complex(self, player: Player, c: Cycle) -> None: | ||
cell_region = pipe(c, face_from_cycle, closure) | ||
if player == Player.BLUE: | ||
self.blue |= cell_region | ||
else: | ||
self.red |= cell_region | ||
|
||
def string_from_homology(self) -> str: | ||
h0b = betti(0, self.blue) | ||
h1b = betti(1, self.blue) | ||
h0r = betti(0, self.red) | ||
h1r = betti(1, self.red) | ||
|
||
h0bb = betti(0, self.blue, self.board.blue_base) | ||
h1bb = betti(1, self.blue, self.board.blue_base) | ||
h0rr = betti(0, self.red, self.board.red_base) | ||
h1rr = betti(1, self.red, self.board.red_base) | ||
|
||
winner = "winner: blue!" if h1bb > h1b else "winner: red!" if h1rr > h1r else "" | ||
|
||
string = ( | ||
f"betti :\tdim=0\tdim=1\n" | ||
f"------------------------------\n" | ||
f"b(B) :\t{h0b}\t{h1b}\n" | ||
f"b(B,B_0) :\t{h0bb}\t{h1bb}\n" | ||
f"b(R) :\t{h0r}\t{h1r}\n" | ||
f"b(R,R_0) :\t{h0rr}\t{h1rr}\n" | ||
f"\n\n" | ||
f"{winner}" | ||
) | ||
|
||
return string |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import tkinter as tk | ||
import tkinter.font | ||
|
||
from more_itertools import collapse | ||
from pyrsistent import pset, s | ||
from pyrsistent.typing import PSet | ||
from toolz.curried import map, pipe | ||
|
||
from board import Board | ||
from game import Game, Player | ||
from my_types import Cycle | ||
from settings import ( | ||
BORDER_SCALE, | ||
COLOR_BG, | ||
COLOR_BLUE, | ||
COLOR_FG, | ||
COLOR_RED, | ||
FONT_SIZE, | ||
OUTLINE_WIDTH, | ||
SCREEN_SIZE, | ||
TITLE, | ||
) | ||
|
||
|
||
class Text: | ||
def __init__(self, master: tk.Tk, game: Game) -> None: | ||
self.game = game | ||
self.string_var = tk.StringVar() | ||
|
||
frame = tk.Frame(master, height=SCREEN_SIZE, width=SCREEN_SIZE) | ||
frame.pack(side="right", fill="both") | ||
frame.pack_propagate(0) | ||
|
||
default_font = tkinter.font.nametofont("TkFixedFont") | ||
default_font.configure(size=FONT_SIZE) | ||
tk.Label( | ||
master=frame, | ||
textvariable=self.string_var, | ||
font=default_font, | ||
bg=COLOR_BG, | ||
fg=COLOR_FG, | ||
justify="left", | ||
).pack(fill="both", expand=1) | ||
|
||
self.string_var.set("New game!") | ||
|
||
def update_text(self) -> None: | ||
self.string_var.set(self.game.string_from_homology()) | ||
|
||
|
||
class Graphics: | ||
__SCALE = (SCREEN_SIZE - 2 * OUTLINE_WIDTH) / BORDER_SCALE | ||
__OFFSET = 0.5 * SCREEN_SIZE | ||
|
||
def __init__(self, game: Game) -> None: | ||
root = tk.Tk() | ||
root.resizable(False, False) | ||
root.title(TITLE) | ||
|
||
self.canvas = tk.Canvas( | ||
root, | ||
height=SCREEN_SIZE, | ||
width=SCREEN_SIZE, | ||
bg=COLOR_BG, | ||
highlightthickness=0, | ||
) | ||
self.game = game | ||
self.text = Text(root, game) | ||
|
||
for player in s(Player.BLUE, Player.RED): | ||
self.generate_base_polygons(game.board, player) | ||
|
||
for c in game.board.cycles: | ||
CyclePolygon(self, game, c) | ||
|
||
self.canvas.pack(side="left", fill="both") | ||
|
||
root.mainloop() | ||
|
||
def polygon_from_cycle( | ||
self, c: Cycle, fill: str = COLOR_FG, state: str = "normal" | ||
) -> int: | ||
flattened_coords = pipe( | ||
self.game.board.points[c], self.unit_coord_to_pixel, collapse, list | ||
) | ||
return self.canvas.create_polygon( | ||
flattened_coords, | ||
fill=fill, | ||
activefill=COLOR_BG, | ||
outline=COLOR_BG, | ||
width=OUTLINE_WIDTH, | ||
state=state, | ||
) | ||
|
||
def polygons_from_cycles(self, cs: PSet[Cycle], fill: str, state: str) -> PSet[int]: | ||
return pipe(cs, map(lambda c: self.polygon_from_cycle(c, fill, state)), pset) | ||
|
||
def generate_base_polygons(self, board: Board, team: Player) -> None: | ||
if team == Player.BLUE: | ||
fill = COLOR_BLUE | ||
cs = board.blue_base_cs | ||
else: | ||
fill = COLOR_RED | ||
cs = board.red_base_cs | ||
self.polygons_from_cycles(cs, fill, "disabled") | ||
|
||
def update_territory(self, polygon: int, player: Player) -> None: | ||
fill = COLOR_BLUE if player == Player.BLUE else COLOR_RED | ||
self.canvas.itemconfigure(polygon, fill=fill, state="disabled") | ||
self.text.update_text() | ||
|
||
def unit_coord_to_pixel(self, coord: float) -> float: | ||
return self.__SCALE * coord + self.__OFFSET | ||
|
||
|
||
class CyclePolygon: | ||
def __init__(self, graphics: Graphics, game: Game, c: Cycle) -> None: | ||
self.c = c | ||
self.game = game | ||
self.graphics = graphics | ||
self.polygon = graphics.polygon_from_cycle(c) | ||
self.graphics.canvas.tag_bind(self.polygon, "<Button-1>", self.claim) | ||
|
||
def claim(self, _: tk.Event) -> None: | ||
player = self.game.player() | ||
self.game.add_cycle_to_player_complex(player, self.c) | ||
self.graphics.update_territory(self.polygon, player) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from board import Board | ||
from game import Game | ||
from gui import Graphics | ||
|
||
|
||
def main() -> None: | ||
board = Board() | ||
game = Game(board) | ||
Graphics(game) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from pyrsistent.typing import PSet, PVector | ||
|
||
Cycle = PVector[int] | ||
Complex = PSet[PSet[int]] |
Oops, something went wrong.