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