Games

Two-player games demonstrating click interactions and game state management.

Features:

  • Click interactions on game board objects

  • Multi-client game state synchronization

  • Turn-based gameplay mechanics

  • Dynamic scene updates for game boards

Source: examples/04_demos/05_games.py

Games

Code

  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.initial_camera.position = (5.0, 3.0, 5.0)
 15    server.initial_camera.look_at = (0.0, 3.0, 2.5)
 16    play_connect_4(server)
 17
 18    server.gui.add_button("Tic-Tac-Toe").on_click(lambda _: play_tic_tac_toe(server))
 19    server.gui.add_button("Connect 4").on_click(lambda _: play_connect_4(server))
 20
 21    while True:
 22        time.sleep(10.0)
 23
 24
 25def play_connect_4(server: viser.ViserServer) -> None:
 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    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        for scale in np.linspace(0.01, 1.0, 5):
100            if symbol == "x":
101                for k in range(2):
102                    server.scene.add_box(
103                        f"/symbols/{i}_{j}/{k}",
104                        dimensions=(0.7 * scale, 0.125 * scale, 0.125),
105                        position=(i, j, 0),
106                        color=(0, 0, 255),
107                        wxyz=tf.SO3.from_z_radians(np.pi / 2.0 * k + np.pi / 4.0).wxyz,
108                    )
109            elif symbol == "o":
110                mesh = trimesh.creation.annulus(0.25 * scale, 0.35 * scale, 0.125)
111                server.scene.add_mesh_simple(
112                    f"/symbols/{i}_{j}",
113                    mesh.vertices,
114                    mesh.faces,
115                    position=(i, j, 0),
116                    color=(255, 0, 0),
117                )
118            else:
119                assert_never(symbol)
120            server.flush()
121            time.sleep(1.0 / 30.0)
122
123    def setup_cell(i: int, j: int) -> None:
124        sphere = server.scene.add_icosphere(
125            f"/spheres/{i}_{j}",
126            radius=0.25,
127            position=(i, j, 0),
128            color=(255, 255, 255),
129        )
130
131        @sphere.on_click
132        def _(_) -> None:
133            nonlocal whose_turn
134            whose_turn = "x" if whose_turn != "x" else "o"
135            sphere.remove()
136            draw_symbol(whose_turn, i, j)
137
138    for i in range(3):
139        for j in range(3):
140            setup_cell(i, j)
141
142
143if __name__ == "__main__":
144    main()