Games#

Some two-player games implemented using scene click events.

  1import time
  2from typing import Literal
  3
  4import numpy as onp
  5import trimesh.creation
  6import viser
  7import viser.transforms as tf
  8from typing_extensions import assert_never
  9
 10
 11def main() -> None:
 12    server = viser.ViserServer()
 13    server.gui.configure_theme(dark_mode=True)
 14    play_connect_4(server)
 15
 16    server.gui.add_button("Tic-Tac-Toe").on_click(lambda _: play_tic_tac_toe(server))
 17    server.gui.add_button("Connect 4").on_click(lambda _: play_connect_4(server))
 18
 19    while True:
 20        time.sleep(10.0)
 21
 22
 23def play_connect_4(server: viser.ViserServer) -> None:
 24    """Play a game of Connect 4."""
 25    server.scene.reset()
 26
 27    num_rows = 6
 28    num_cols = 7
 29
 30    whose_turn: Literal["red", "yellow"] = "red"
 31    pieces_in_col = [0] * num_cols
 32
 33    # Create the board frame.
 34    for col in range(num_cols):
 35        for row in range(num_rows):
 36            server.scene.add_mesh_trimesh(
 37                f"/structure/{row}_{col}",
 38                trimesh.creation.annulus(0.45, 0.55, 0.125),
 39                position=(0.0, col, row),
 40                wxyz=tf.SO3.from_y_radians(onp.pi / 2.0).wxyz,
 41            )
 42
 43    # Create a sphere to click on for each column.
 44    def setup_column(col: int) -> None:
 45        sphere = server.scene.add_icosphere(
 46            f"/spheres/{col}",
 47            radius=0.25,
 48            position=(0, col, num_rows - 0.25),
 49            color=(255, 255, 255),
 50        )
 51
 52        # Drop piece into the column.
 53        @sphere.on_click
 54        def _(_) -> None:
 55            nonlocal whose_turn
 56            whose_turn = "red" if whose_turn != "red" else "yellow"
 57
 58            row = pieces_in_col[col]
 59            if row == num_rows - 1:
 60                sphere.remove()
 61
 62            pieces_in_col[col] += 1
 63            cylinder = trimesh.creation.cylinder(radius=0.4, height=0.125)
 64            piece = server.scene.add_mesh_simple(
 65                f"/game_pieces/{row}_{col}",
 66                cylinder.vertices,
 67                cylinder.faces,
 68                wxyz=tf.SO3.from_y_radians(onp.pi / 2.0).wxyz,
 69                color={"red": (255, 0, 0), "yellow": (255, 255, 0)}[whose_turn],
 70            )
 71            for row_anim in onp.linspace(num_rows - 1, row, num_rows - row + 1):
 72                piece.position = (
 73                    0,
 74                    col,
 75                    row_anim,
 76                )
 77                time.sleep(1.0 / 30.0)
 78
 79    for col in range(num_cols):
 80        setup_column(col)
 81
 82
 83def play_tic_tac_toe(server: viser.ViserServer) -> None:
 84    """Play a game of tic-tac-toe."""
 85    server.scene.reset()
 86
 87    whose_turn: Literal["x", "o"] = "x"
 88
 89    for i in range(4):
 90        server.scene.add_spline_catmull_rom(
 91            f"/gridlines/{i}",
 92            ((-0.5, -1.5, 0), (-0.5, 1.5, 0)),
 93            color=(127, 127, 127),
 94            position=(1, 1, 0),
 95            wxyz=tf.SO3.from_z_radians(onp.pi / 2 * i).wxyz,
 96        )
 97
 98    def draw_symbol(symbol: Literal["x", "o"], i: int, j: int) -> None:
 99        """Draw an X or O in the given cell."""
100        for scale in onp.linspace(0.01, 1.0, 5):
101            if symbol == "x":
102                for k in range(2):
103                    server.scene.add_box(
104                        f"/symbols/{i}_{j}/{k}",
105                        dimensions=(0.7 * scale, 0.125 * scale, 0.125),
106                        position=(i, j, 0),
107                        color=(0, 0, 255),
108                        wxyz=tf.SO3.from_z_radians(
109                            onp.pi / 2.0 * k + onp.pi / 4.0
110                        ).wxyz,
111                    )
112            elif symbol == "o":
113                mesh = trimesh.creation.annulus(0.25 * scale, 0.35 * scale, 0.125)
114                server.scene.add_mesh_simple(
115                    f"/symbols/{i}_{j}",
116                    mesh.vertices,
117                    mesh.faces,
118                    position=(i, j, 0),
119                    color=(255, 0, 0),
120                )
121            else:
122                assert_never(symbol)
123            server.flush()
124            time.sleep(1.0 / 30.0)
125
126    def setup_cell(i: int, j: int) -> None:
127        """Create a clickable sphere in a given cell."""
128        sphere = server.scene.add_icosphere(
129            f"/spheres/{i}_{j}",
130            radius=0.25,
131            position=(i, j, 0),
132            color=(255, 255, 255),
133        )
134
135        @sphere.on_click
136        def _(_) -> None:
137            nonlocal whose_turn
138            whose_turn = "x" if whose_turn != "x" else "o"
139            sphere.remove()
140            draw_symbol(whose_turn, i, j)
141
142    for i in range(3):
143        for j in range(3):
144            setup_cell(i, j)
145
146
147if __name__ == "__main__":
148    main()