Skip to content

Commit

Permalink
first
Browse files Browse the repository at this point in the history
  • Loading branch information
nasosev committed Jul 14, 2019
1 parent 451d55e commit 17cd9a2
Show file tree
Hide file tree
Showing 12 changed files with 922 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,8 @@ dmypy.json

# Pyre type checker
.pyre/

# JetBrains
.idea/
notebooks/
.backup/
16 changes: 15 additions & 1 deletion README.md
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).
117 changes: 117 additions & 0 deletions board.py
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
59 changes: 59 additions & 0 deletions game.py
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
127 changes: 127 additions & 0 deletions gui.py
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)
13 changes: 13 additions & 0 deletions main.py
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()
4 changes: 4 additions & 0 deletions my_types.py
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]]
Loading

0 comments on commit 17cd9a2

Please sign in to comment.