GamesΒΆ

Some two-player games implemented using scene click events.

  1import time
  2from typing import Literal
  3
  4import numpy as np
  5import trimesh.creation
  6from typing_extensions import assert_never
  7
  8import viser
  9import viser.transforms as tf
 10
 11
 12def main() -> None:
 13    server = viser.ViserServer()
 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(np.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(np.pi / 2.0).wxyz,
 69                color={"red": (255, 0, 0), "yellow": (255, 255, 0)}[whose_turn],
 70            )
 71            for row_anim in np.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(np.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 np.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(np.pi / 2.0 * k + np.pi / 4.0).wxyz,
109                    )
110            elif symbol == "o":
111                mesh = trimesh.creation.annulus(0.25 * scale, 0.35 * scale, 0.125)
112                server.scene.add_mesh_simple(
113                    f"/symbols/{i}_{j}",
114                    mesh.vertices,
115                    mesh.faces,
116                    position=(i, j, 0),
117                    color=(255, 0, 0),
118                )
119            else:
120                assert_never(symbol)
121            server.flush()
122            time.sleep(1.0 / 30.0)
123
124    def setup_cell(i: int, j: int) -> None:
125        """Create a clickable sphere in a given cell."""
126        sphere = server.scene.add_icosphere(
127            f"/spheres/{i}_{j}",
128            radius=0.25,
129            position=(i, j, 0),
130            color=(255, 255, 255),
131        )
132
133        @sphere.on_click
134        def _(_) -> None:
135            nonlocal whose_turn
136            whose_turn = "x" if whose_turn != "x" else "o"
137            sphere.remove()
138            draw_symbol(whose_turn, i, j)
139
140    for i in range(3):
141        for j in range(3):
142            setup_cell(i, j)
143
144
145if __name__ == "__main__":
146    main()