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()