cuju-render 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,759 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Watches cuju's matches in a browser, saved or live, in 3D with sound.
5
+
6
+ The renderer reads GRF states and never writes one, so a match plays the same bits watched or not.
7
+ `show` opens the viewer on a batch, a recording or a live source in one call, starting the server
8
+ when none runs. `Recorder` and `record` save the matches of a batch, `watch` streams one
9
+ environment of a running batch, `add_team` registers a Kaggle submission folder, `play` records a
10
+ match between two teams of the registry, `challenge` plays a challenge headless with a team or a
11
+ policy, and `serve` runs the local page that plays them all. `feed_url` gives the address of a
12
+ clean feed for a streaming program such as OBS. The guide is in the repository's
13
+ `docs/rendering.md`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import struct
20
+ import threading
21
+ import weakref
22
+ from collections.abc import Callable
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from cuju.errors import ArgumentError
27
+
28
+ from ._format import annotations as _annotations
29
+ from ._format import keep as _keep
30
+ from ._format import recording as _recording
31
+ from ._format import writer as _writer
32
+ from ._http.server import serve
33
+ from ._playing.errors import PolicyError
34
+ from ._registry.teams import add_folder as add_team
35
+ from ._store import library as _library
36
+ from ._store.home import home
37
+ from ._text import messages as _messages
38
+ from ._watch import live as _live
39
+ from ._watch.live import LiveSource
40
+ from ._watch.show import View, show
41
+
42
+ __all__ = [
43
+ 'LiveSource',
44
+ 'PolicyError',
45
+ 'Recorder',
46
+ 'View',
47
+ 'add_team',
48
+ 'captions',
49
+ 'challenge',
50
+ 'challenges',
51
+ 'delete_group',
52
+ 'feed_url',
53
+ 'home',
54
+ 'on_batch_created',
55
+ 'play',
56
+ 'record',
57
+ 'recordings',
58
+ 'restore_group',
59
+ 'search',
60
+ 'serve',
61
+ 'show',
62
+ 'watch',
63
+ ]
64
+
65
+ _LOGGER = logging.getLogger('cuju')
66
+
67
+
68
+ class Recorder:
69
+ """Records the matches of one environment of a batch into the renderer's library.
70
+
71
+ The recorder is a step observer of the batch, so it captures every `reset` and step by itself.
72
+ A recording ends when its episode ends, and `save` writes it, so a batch with automatic resets
73
+ yields one recording per episode. Code that calls `capture` after each step, as with cuju
74
+ 0.2.0, still works, since the first call of `capture` turns the automatic capture off.
75
+
76
+ Filters keep only the episodes worth watching. At the end of each episode the recorder reads
77
+ its summary, and an episode that a filter drops is discarded without writing anything.
78
+ `annotate` attaches numbers and text of a training loop or a policy to the captured steps.
79
+
80
+ The step that ends an episode only hands its frames over, and one thread of the renderer codes
81
+ and writes the recording while training steps on. `save` and closing the batch wait for the
82
+ recordings handed over, and `flush` waits for them alone.
83
+
84
+ Attributes:
85
+ environment: The index of the environment.
86
+ saved: The paths of the recordings written so far, each added once its file is written.
87
+ version: The version of the recording files.
88
+ episodes: The number of episodes that ended, saved or dropped.
89
+ dropped: The number of episodes that the filters dropped.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ batch: Any,
95
+ environment: int = 0,
96
+ title: str | None = None,
97
+ teams: list[dict[str, Any]] | None = None,
98
+ scenario: str | None = None,
99
+ version: int = 2,
100
+ keep: str | Callable[[dict[str, Any]], Any] | None = None,
101
+ every: int = 1,
102
+ min_reward: float | None = None,
103
+ ) -> None:
104
+ """Attaches a recorder of `environment` to `batch`, which captures from the next call on.
105
+
106
+ Args:
107
+ batch: The batch.
108
+ environment: The environment to record.
109
+ title: The title of each recording, by default the scenario and the environment.
110
+ teams: The name, `color` and `secondary` color of the left and right team, each an
111
+ RGB list of three integers from 0 to 255, each optional.
112
+ scenario: The name of the scenario that the header records, by default the batch's.
113
+ version: The version of the recording files, 2, or 1 for the readers of cuju 0.2.0,
114
+ which takes about two and a half times the room.
115
+ keep: Which episodes to save, `all` or `None` for every one, `goals` for those with a
116
+ goal, or a function that takes the summary of the episode and returns whether to
117
+ save it. The summary holds the `episode` number, counted from 0 over the episodes
118
+ that the recorder ended, the `environment`, the `score` of the left and right
119
+ team, the number of `goals`, the `length` in frames, the last `steps` number, the
120
+ `rewards` of each agent summed over the captured steps, their sum, `reward`, and
121
+ the `annotations` of the episode, each with its `frame`, `step`, `player`,
122
+ `values`, `text`, `action` and `probabilities`, the player numbered as
123
+ `annotate` takes it.
124
+ every: Saves the first episode and every `every`-th after it, counting every episode
125
+ that ends, saved or not.
126
+ min_reward: Saves only episodes whose `reward` is at least this.
127
+
128
+ Raises:
129
+ ArgumentError: If the batch is not a `BatchEnvironment`, the environment is not one of
130
+ the batch, or a version or filter is invalid.
131
+ """
132
+ if not callable(getattr(batch, 'add_observer', None)):
133
+ raise ArgumentError(
134
+ 'expected a BatchEnvironment, found a batch whose states the recorder cannot read; '
135
+ 'record one of the batches of a MultiDeviceBatch instead'
136
+ )
137
+ if not 0 <= environment < batch.environment_count:
138
+ raise ArgumentError(
139
+ f'expected an environment from 0 to {batch.environment_count - 1}, '
140
+ f'found {environment}'
141
+ )
142
+ if version not in _recording.VERSIONS or isinstance(version, bool):
143
+ raise ArgumentError(f'expected a version from {_recording.VERSIONS}, found {version!r}')
144
+ self.version = version
145
+ self._filter = _keep.Filter(keep, every, min_reward)
146
+ # The rewards of the episode, the episodes ended so far and those dropped.
147
+ self._rewards: list[float] = []
148
+ self.episodes = 0
149
+ self.dropped = 0
150
+ # The recordings handed to the writer and not written yet, and the error of a failed
151
+ # write, which the next `save` or `flush` raises.
152
+ self._pending = 0
153
+ self._written = threading.Condition()
154
+ self._failure: Exception | None = None
155
+ # The annotations of the recording, by frame and player.
156
+ self._notes: dict[tuple[int, int], dict[str, Any]] = {}
157
+ # The batch holds the recorder, which holds the batch weakly, so that a dropped batch
158
+ # frees its memory without waiting for the garbage collector.
159
+ self._batch = weakref.ref(batch)
160
+ self.environment = environment
161
+ self._title = title
162
+ self._teams = teams
163
+ self._scenario = scenario or _live.scenario_name(batch)
164
+ players = int(batch.left_players[environment] + batch.right_players[environment])
165
+ self._reader = _recording.StateReader(players)
166
+ self._lineup: list[dict[str, Any]] | None = None
167
+ self._frames: list[bytes] = []
168
+ self._timing = None
169
+ self.saved: list[Path] = []
170
+ # The information of the last step, whose scoring signal completes an ending episode.
171
+ self._information: dict[str, Any] | None = None
172
+ # Whether the caller captures by hand, as with cuju 0.2.0, which ends automatic capture.
173
+ self._manual = False
174
+ batch.add_observer(self)
175
+
176
+ def __repr__(self) -> str:
177
+ batch = self._batch()
178
+ if batch is None:
179
+ state = 'its batch gone'
180
+ elif any(observer is self for observer in batch.observers):
181
+ state = 'by hand' if self._manual else 'capturing'
182
+ else:
183
+ state = 'detached'
184
+ return (
185
+ f'Recorder(environment {self.environment}, {self._scenario!r}, version {self.version}, '
186
+ f'{state}, {len(self._frames)} frames of the current episode, {self.episodes} '
187
+ f'episodes ended, {len(self.saved)} saved, {self.dropped} dropped)'
188
+ )
189
+
190
+ def __call__(self, batch: Any, done: Any, information: dict[str, Any]) -> None:
191
+ """Captures a reset of the environment or a step, as a step observer of the batch.
192
+
193
+ A batch that closes waits for the recordings handed to the writer.
194
+ """
195
+ if done is None:
196
+ if information.get('close'):
197
+ self._wait()
198
+ return
199
+ if self._manual or 'reset' not in information:
200
+ return
201
+ reset = information['reset']
202
+ if reset is None or bool(reset[self.environment]):
203
+ self._capture(batch, None)
204
+ return
205
+ self._information = information
206
+ if not self._manual:
207
+ self._capture(batch, done)
208
+
209
+ def capture(self, done: Any = None) -> Path | None:
210
+ """Adds the environment's current step by hand, and saves the recording when its episode
211
+ ended.
212
+
213
+ The recorder captures by itself, so a program needs this only to choose its steps. The
214
+ first call turns the automatic capture off for good and drops the frames that it took and
215
+ has not saved, so code written for cuju 0.2.0, which calls this after `reset` and after
216
+ each `step`, records the same frames as before.
217
+
218
+ With automatic resets, the step that ends an episode already shows the next one's start,
219
+ so the recording ends at the step before, marked as the end, and the new start opens the
220
+ next recording.
221
+
222
+ Args:
223
+ done: The done flags that `step` returned, or `None` after `reset`.
224
+
225
+ Returns:
226
+ The path of the recording that this call hands to the writer, which `saved` lists
227
+ once its file is written, or `None`, also for an episode that the filters dropped.
228
+
229
+ Raises:
230
+ ArgumentError: If the batch is gone.
231
+ """
232
+ if not self._manual:
233
+ self._manual = True
234
+ self._frames, self._lineup, self._rewards, self._notes = [], None, [], {}
235
+ batch = self._batch()
236
+ if batch is None:
237
+ raise ArgumentError('expected the batch of the recorder, found it gone')
238
+ return self._capture(batch, done)
239
+
240
+ def _capture(self, batch: Any, done: Any) -> Path | None:
241
+ """Adds the environment's current step, as `capture` describes."""
242
+ ended = done is not None and bool(done[self.environment])
243
+ if done is not None:
244
+ # The step's rewards belong to the episode that it ends, if it ends one.
245
+ rewards = _keep.step_rewards(batch, self.environment, self._information or {})
246
+ if not rewards:
247
+ rewards = [0.0] * len(self._rewards)
248
+ self._rewards = [
249
+ total + reward
250
+ for total, reward in zip(
251
+ self._rewards or [0.0] * len(rewards), rewards, strict=True
252
+ )
253
+ ]
254
+ saved = None
255
+ if ended and batch.auto_reset:
256
+ if self._frames:
257
+ last = bytearray(self._frames[-1])
258
+ (flags,) = struct.unpack_from('<i', last, 28)
259
+ struct.pack_into('<i', last, 28, flags | 1)
260
+ # GRF: the scoring signal is 1 when the left team scores and -1 for the right.
261
+ information = self._information or {}
262
+ signal = information.get('score_reward')
263
+ scored = int(signal[self.environment]) if signal is not None else 0
264
+ if scored:
265
+ offset = 8 if scored > 0 else 12
266
+ (goals,) = struct.unpack_from('<i', last, offset)
267
+ struct.pack_into('<i', last, offset, goals + 1)
268
+ struct.pack_into('<i', last, 28, (flags | 1) & ~(31 << 8))
269
+ self._frames[-1] = bytes(last)
270
+ saved = self._finish()
271
+ state = batch.get_state(self.environment, stacked=batch.stacked)
272
+ self._timing = _recording.scenario_timing(state)
273
+ if self._lineup is None:
274
+ self._lineup = self._reader.lineup(state)
275
+ closing = ended and not batch.auto_reset
276
+ self._frames.append(self._reader.frame(state, 1 if closing else 0))
277
+ if closing:
278
+ saved = self._finish()
279
+ return saved
280
+
281
+ def _finish(self) -> Path | None:
282
+ """Ends the episode, which the filters save or drop, and returns the path of a save."""
283
+ if not self._frames or self._lineup is None:
284
+ self._rewards = []
285
+ return None
286
+ notes = [
287
+ _annotations.public(note, self._frames, self._lineup) for note in self._notes.values()
288
+ ]
289
+ summary = _keep.episode_summary(
290
+ self.episodes, self._frames, self._rewards, self.environment, notes
291
+ )
292
+ self.episodes += 1
293
+ if self._filter(summary):
294
+ return self._hand_over()
295
+ self._frames, self._lineup, self._rewards, self._notes = [], None, [], {}
296
+ self.dropped += 1
297
+ return None
298
+
299
+ def annotate(
300
+ self,
301
+ values: dict[str, Any] | None = None,
302
+ text: str | None = None,
303
+ player: Any = None,
304
+ step: int | None = None,
305
+ probabilities: Any = None,
306
+ action: Any = None,
307
+ ) -> None:
308
+ """Attaches numbers or text to a captured step, which the page shows with the recording.
309
+
310
+ Annotating reads nothing of the batch and changes nothing of it. Several calls for one
311
+ step and player join their numbers, and a later text, action or probabilities replace the
312
+ earlier ones. A recording of version 1 has no room for annotations and leaves them out.
313
+
314
+ Args:
315
+ values: Named numbers, such as `{'value': 0.4, 'reward': 1.0}`, at most 16 names of at
316
+ most 40 characters. Each number is kept to six significant digits.
317
+ text: A short text, cut at 200 characters.
318
+ player: The player, a number that counts the left team's players first and then the
319
+ right team's, as the batch counts its agents, or a pair of team, 0 for the left,
320
+ and the index within the team, or `None` for the whole match. The summary that a
321
+ filter reads names the player by the same number.
322
+ step: The step of the current episode, as the frame's step counts it, by default the
323
+ step captured last.
324
+ probabilities: The probability of each action, a sequence indexed by action or a
325
+ mapping of action to probability. The three most likely actions are kept.
326
+ action: The action chosen.
327
+
328
+ Raises:
329
+ ArgumentError: If no step is captured yet, the step is not one of the current
330
+ episode, or a part is invalid.
331
+ """
332
+ if not self._frames or self._lineup is None:
333
+ raise ArgumentError('expected a captured step to annotate, found none')
334
+ record = _annotations.note(self._lineup, values, text, player, probabilities, action)
335
+ _annotations.merge(self._notes, _annotations.frame_of(self._frames, step), record)
336
+
337
+ def save(self) -> Path | None:
338
+ """Writes the frames captured so far as one recording, starts a new one, and waits until
339
+ every recording handed to the writer is written.
340
+
341
+ A call by hand saves whatever the filters say, as a training run that stops may wish.
342
+
343
+ Returns:
344
+ The path of the recording, or `None` when nothing was captured.
345
+
346
+ Raises:
347
+ Exception: The error of a write that failed since the last `save` or `flush`.
348
+ """
349
+ path = self._hand_over()
350
+ self.flush()
351
+ return path
352
+
353
+ def flush(self) -> None:
354
+ """Waits until every recording handed to the writer is written.
355
+
356
+ Raises:
357
+ Exception: The error of a write that failed since the last `save` or `flush`, which
358
+ the logger `cuju` also reported when it happened.
359
+ """
360
+ self._wait()
361
+ with self._written:
362
+ failure, self._failure = self._failure, None
363
+ if failure is not None:
364
+ raise failure
365
+
366
+ def _wait(self) -> None:
367
+ """Waits for the writes of the recorder."""
368
+ with self._written:
369
+ self._written.wait_for(lambda: self._pending == 0)
370
+
371
+ def _hand_over(self) -> Path | None:
372
+ """Hands the frames captured so far to the writer, and returns the path they will take.
373
+
374
+ The step pays for the header and the name of the file alone, and the writer codes and
375
+ writes the recording. A writer that is busy with several recordings makes this wait for
376
+ room.
377
+ """
378
+ if not self._frames or self._lineup is None:
379
+ return None
380
+ # A title of the recorder's own is a label, which the page shows in its own language.
381
+ title = self._title or _messages.label(
382
+ 'server.names.recorder',
383
+ scenario=_messages.scenario(self._scenario),
384
+ environment=self.environment,
385
+ )
386
+ # A batch may have been reset with another seed before the capture, so the recording
387
+ # names its environment alone and leaves the seed unknown.
388
+ head = _recording.header(
389
+ title,
390
+ self._scenario,
391
+ self._lineup,
392
+ self._teams,
393
+ len(self._frames),
394
+ self._timing,
395
+ {'source': 'recorder', 'environment': self.environment},
396
+ )
397
+ frames, notes = self._frames, list(self._notes.values())
398
+ self._frames, self._lineup, self._rewards, self._notes = [], None, [], {}
399
+ path = _library.reserve()
400
+ version = self.version
401
+
402
+ def write() -> None:
403
+ try:
404
+ _library.save(head, frames, path.stem, version, notes)
405
+ except Exception as error:
406
+ if not path.exists():
407
+ path.with_suffix('.json').unlink(missing_ok=True)
408
+ _LOGGER.warning(
409
+ 'cuju render: the recorder could not write %s, %s: %s',
410
+ path.name,
411
+ type(error).__name__,
412
+ error,
413
+ )
414
+ with self._written:
415
+ self._failure = error
416
+ else:
417
+ self.saved.append(path)
418
+ finally:
419
+ with self._written:
420
+ self._pending -= 1
421
+ self._written.notify_all()
422
+
423
+ with self._written:
424
+ self._pending += 1
425
+ _writer.WRITER.submit(write)
426
+ return path
427
+
428
+ def close(self) -> Path | None:
429
+ """Detaches the recorder from its batch, saves the frames captured so far and waits until
430
+ every recording handed to the writer is written.
431
+
432
+ Returns:
433
+ The path of the recording that this call saved, or `None` when nothing was captured.
434
+
435
+ Raises:
436
+ Exception: The error of a write that failed since the last `save` or `flush`.
437
+ """
438
+ batch = self._batch()
439
+ if batch is not None and any(observer is self for observer in batch.observers):
440
+ batch.remove_observer(self)
441
+ return self.save()
442
+
443
+ def __enter__(self) -> Recorder:
444
+ return self
445
+
446
+ def __exit__(self, *exception: object) -> None:
447
+ """Closes the recorder, so that a `with` block saves its last episode at its end."""
448
+ self.close()
449
+
450
+
451
+ def record(batch: Any, environment: int = 0, **options: Any) -> Recorder:
452
+ """Records the matches of one environment of a batch, as a `Recorder` that also serves as a
453
+ context manager.
454
+
455
+ Leaving the `with` block detaches the recorder and saves the episode that it was capturing,
456
+ and every episode that ended inside the block is saved already.
457
+
458
+ Args:
459
+ batch: The batch.
460
+ environment: The environment to record.
461
+ **options: The other arguments of `Recorder`, such as `title` and `teams`.
462
+
463
+ Returns:
464
+ The recorder, whose list `saved` holds the paths of its recordings.
465
+ """
466
+ return Recorder(batch, environment, **options)
467
+
468
+
469
+ def play(
470
+ left: str,
471
+ right: str,
472
+ scenario: str = '11_vs_11_kaggle',
473
+ seed: int | None = None,
474
+ title: str | None = None,
475
+ device: str = 'cpu',
476
+ ability: tuple[float | None, float | None] = (None, None),
477
+ ) -> Path:
478
+ """Plays a match between two teams of the registry, saves it and returns the recording.
479
+
480
+ Args:
481
+ left: The team of the left side, a registry identifier or `builtin`.
482
+ right: The team of the right side.
483
+ scenario: A shipped scenario, without agent counts, which the teams decide.
484
+ seed: The seed of the match, or `None` for one drawn from the clock.
485
+ title: The title of the recording.
486
+ device: `cpu` or `cuda`.
487
+ ability: The ability of the built-in AI on the left and on the right, each from 0 to 1, or
488
+ `None` for the scenario's, GRF's team difficulty for every player the built-in AI
489
+ controls.
490
+
491
+ Raises:
492
+ ArgumentError: If a team is unknown, plays another format than the scenario's, or an
493
+ ability lies outside 0 to 1.
494
+ """
495
+ from ._playing.play import play as run
496
+
497
+ return run(left, right, scenario, seed, title, device, ability)
498
+
499
+
500
+ def challenge(
501
+ challenge: str,
502
+ team: str | None = None,
503
+ policy: Any = None,
504
+ name: str | None = None,
505
+ control: dict[str, Any] | None = None,
506
+ attempts: int | None = None,
507
+ device: str = 'cpu',
508
+ progress: Any = None,
509
+ keep_recordings: bool = True,
510
+ fail_attempts: bool = False,
511
+ ) -> dict[str, Any]:
512
+ """Plays a challenge headless with a team of the registry or a Python policy, and scores it.
513
+
514
+ Every attempt plays the challenge's seed of its index, as fast as the policy answers, and is
515
+ saved as a recording, and the recordings of the run form one group of the library. A full run
516
+ reaches the challenge's leaderboard, and a run of fewer attempts is practice. A team of one
517
+ dynamic agent faces the very episodes that a person faces on the Play page.
518
+
519
+ Args:
520
+ challenge: The challenge, an identifier that `challenges` lists, such as
521
+ `empty_goal_close`.
522
+ team: A team of the registry, or `builtin` for GRF's built-in AI on the whole side.
523
+ policy: A Kaggle agent function, `agent(obs)` or `agent(obs, configuration)`, in place of
524
+ a team.
525
+ name: The name of a policy on the leaderboard, by default the function's name.
526
+ control: The control of a policy, as a team's control settings, by default one player of
527
+ dynamic assignment.
528
+ attempts: The attempts to play, by default the challenge's count.
529
+ device: `cpu` or `cuda`.
530
+ progress: A function called with the result of each attempt as it ends.
531
+ keep_recordings: Whether the library keeps the attempts as recordings. A run with false
532
+ scores, ranks and earns exactly as one that keeps them, and adds nothing to the library.
533
+ fail_attempts: Whether a policy that fails ends its attempt alone, which then counts as a
534
+ failed attempt with the policy's error, while the run goes on, as the page, the command
535
+ line and the benchmarks play.
536
+
537
+ Returns:
538
+ A plain dictionary of the run, its `value`, `seconds`, `medal`, `rank`, `personal_best`,
539
+ whether it was `practice`, its `medals` as items, its count of `failed_attempts`, the
540
+ result and recording of each of its `attempts`, `None` for a run without recordings, and
541
+ the `group` of its recordings or `None`.
542
+
543
+ Raises:
544
+ ArgumentError: If the challenge, the team, the control or the attempts do not fit.
545
+ PolicyError: If the policy fails, answers with anything but actions, or stops answering,
546
+ unless `fail_attempts` is true.
547
+ """
548
+ from ._playing.challenges import run
549
+
550
+ return run(
551
+ challenge,
552
+ team,
553
+ policy,
554
+ name,
555
+ control,
556
+ attempts,
557
+ device,
558
+ progress,
559
+ keep_recordings,
560
+ fail_attempts,
561
+ )
562
+
563
+
564
+ def challenges() -> list[dict[str, Any]]:
565
+ """Returns the catalog of challenges, each with its goal, measure, targets and seeds."""
566
+ from ._playing.challenges import challenges as catalog
567
+
568
+ return catalog()
569
+
570
+
571
+ def recordings(group: str | None = None, trash: bool = False) -> list[dict[str, Any]]:
572
+ """Returns the summaries of the library, as the Recordings page lists them, newest first.
573
+
574
+ Each summary of a recording of a challenge run holds its `group`, with the run's identifier
575
+ `id`, its `title`, `competitor` and `result`, the `attempt` of the recording and the run's
576
+ `attempts`, `state`, `value`, `seconds`, `medal` and `practice`.
577
+
578
+ Args:
579
+ group: The identifier of a group, to list its recordings alone in the order of their
580
+ attempts.
581
+ trash: Whether to list the Trash in place of the library.
582
+ """
583
+ return _library.matches(trash, group)
584
+
585
+
586
+ def delete_group(group: str) -> list[str]:
587
+ """Moves every recording of a group to the Trash at once, and returns their identifiers.
588
+
589
+ Each recording restores from the Trash like any other, and `restore_group` restores them all.
590
+ An unknown group, or one already in the Trash, returns an empty list.
591
+ """
592
+ return _library.trash_group(group)
593
+
594
+
595
+ def restore_group(group: str) -> list[str]:
596
+ """Restores every recording of a group from the Trash, and returns their identifiers."""
597
+ return _library.trash_group(group, restore=True)
598
+
599
+
600
+ def search(query: str = '', limit: int | None = None) -> dict[str, Any]:
601
+ """Finds moments across every recording of the library, as `cuju render search` does.
602
+
603
+ The query holds words that name kinds of moments, such as `goal`, `shot` or `corner`, other
604
+ words that match the title, scenario, teams and players, and filters such as `team:Greens`,
605
+ `player:9`, `after:60`, `score:trailing` or `source:tournament`, as the guide describes under
606
+ Searching moments.
607
+
608
+ Args:
609
+ query: The query, such as `goal team:Greens after:60`.
610
+ limit: The most results, by default the query's `limit:` or 50.
611
+
612
+ Returns:
613
+ The `results`, each with the recording as `match`, the `kind`, the time `t` in seconds of
614
+ the recording, the `seek` time a few seconds before it that opens Watch, the `minute` of
615
+ the clock, the team, the player's `number` and `name`, the `score`, a plain line of
616
+ `text` and the address `url` of the page, and the `total` before the limit.
617
+ """
618
+ from ._moments import search as _search
619
+
620
+ return _search.search(query, limit)
621
+
622
+
623
+ def captions(recording: str | Path, path: str | Path | None = None) -> list[dict[str, Any]]:
624
+ """Returns the caption cues of a recording and writes them as WebVTT when given a path.
625
+
626
+ Args:
627
+ recording: A recording identifier of the library, such as `sample-5v5`, or a `.cjr` file.
628
+ path: A file to write the captions to as WebVTT, or `None`.
629
+
630
+ Returns:
631
+ The cues, each with its `start` and `end` in seconds to the millisecond, its message `id`
632
+ and `params` of the table of captions and its `text`.
633
+ """
634
+ from ._moments import search as _search
635
+
636
+ cues = _search.captions(recording)
637
+ if path is not None:
638
+ text = _search.captions_vtt(recording)
639
+ Path(path).write_text(text, encoding='utf-8', newline='\n')
640
+ return cues
641
+
642
+
643
+ def watch(
644
+ batch: Any,
645
+ environment: int = 0,
646
+ title: str | None = None,
647
+ teams: list[dict[str, Any]] | None = None,
648
+ backlog: bool = False,
649
+ ) -> LiveSource:
650
+ """Streams one environment of a running batch to the renderer's page.
651
+
652
+ The source is a step observer of the batch, which publishes after each step while a viewer
653
+ watches, as `CUJU_RENDER` makes every batch do. Close the source, or the batch, to withdraw it.
654
+
655
+ Args:
656
+ batch: The batch.
657
+ environment: The environment to show.
658
+ title: The title that the page lists.
659
+ teams: The names and colors of the left and right team, as for `Recorder`.
660
+ backlog: Whether to read every step, watched or not, so that any viewer starts with ten
661
+ seconds of history, at the cost of one state read per step.
662
+
663
+ Returns:
664
+ The source, which the batch holds as one of its observers, in place of any source that
665
+ `watch` or `CUJU_RENDER` gave it before.
666
+
667
+ Raises:
668
+ ArgumentError: If the batch shows no live source or `environment` is not one of its
669
+ environments, which leaves any earlier source of the batch as it was.
670
+ """
671
+ if not callable(getattr(batch, 'add_observer', None)):
672
+ raise ArgumentError(
673
+ 'expected a BatchEnvironment, found a batch whose steps show no live source; watch '
674
+ 'one of the batches of a MultiDeviceBatch instead'
675
+ )
676
+ # The environment is checked before the batch's earlier source closes, so a mistake leaves it.
677
+ _live.check_environment(batch, environment)
678
+ for observer in batch.observers:
679
+ if isinstance(observer, LiveSource):
680
+ observer.close()
681
+ source = LiveSource(batch, environment, title, teams, backlog=backlog)
682
+ batch.add_observer(source)
683
+ return source
684
+
685
+
686
+ def feed_url(source: str = 'auto', *, port: int | None = None, **options: Any) -> str:
687
+ """Returns the address of a clean feed, a picture of fixed size for a streaming program.
688
+
689
+ The address names the page at `/feed` of the renderer's server, the running server of the
690
+ renderer's folder, or the server on `port`, 8780 by default, when none runs. The feed shows
691
+ the Watch page with its broadcast graphics alone and follows its source, as `cuju render feed`
692
+ prints it.
693
+
694
+ Args:
695
+ source: What the feed follows, `auto`, `live`, `latest`, `tournament`, `queue`,
696
+ `live:NAME`, `tournament:ID` or the identifier of a recording.
697
+ port: The port of the server, when no server of the renderer's folder runs.
698
+ **options: The options of the feed's address, such as `size='vertical'`, `width` and
699
+ `height`, `scale`, `safe`, `bg`, `layer`, `audio`, `volume`, `fps`, `camera`, `loop`,
700
+ `broadcast`, `show`, `score`, `slate`, `quality`, `theme`, `arena`, `time` and `t`.
701
+
702
+ Raises:
703
+ ArgumentError: If the source or an option is out of place.
704
+ """
705
+ from ._export import feed as _feed
706
+ from ._http import launch as _launch
707
+
708
+ server = _launch.running() if port is None else None
709
+ base = server.url.split('#')[0] if server is not None else f'http://127.0.0.1:{port or 8780}/'
710
+ return _feed.address(base, source=source, **options)
711
+
712
+
713
+ def on_batch_created(batch: Any) -> LiveSource | None:
714
+ """Gives a new batch the live source that `CUJU_RENDER` asks for, and returns it.
715
+
716
+ cuju calls this for every batch that it creates while `CUJU_RENDER` is set, and it is the one
717
+ function of the renderer that cuju calls. `CUJU_RENDER` holds `on` for environment 0, or the
718
+ index of the environment to show, and an index past the batch's count shows environment 0.
719
+ A non-empty `CUJU_RENDER_BACKLOG` other than `0`, `off` or `no` makes the source read every
720
+ step.
721
+
722
+ Args:
723
+ batch: The batch.
724
+
725
+ Returns:
726
+ The source, or `None` when the variable asks for none.
727
+ """
728
+ source = _live.from_environment(batch)
729
+ if source is not None:
730
+ batch.add_observer(source)
731
+ return source
732
+
733
+
734
+ # The modules keep `cuju.render` as the module of the public names.
735
+ for _name in (
736
+ PolicyError,
737
+ Recorder,
738
+ watch,
739
+ on_batch_created,
740
+ home,
741
+ play,
742
+ serve,
743
+ LiveSource,
744
+ add_team,
745
+ show,
746
+ View,
747
+ challenge,
748
+ challenges,
749
+ feed_url,
750
+ ):
751
+ _name.__module__ = __name__
752
+ del _name
753
+ # The errors that only add the id of a message read as the public class they stand for.
754
+ _messages.public_names()
755
+
756
+
757
+ def __dir__() -> list[str]:
758
+ """Returns the public names of the module, those of `__all__`."""
759
+ return sorted(__all__)