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,1065 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The command line of the renderer, `cuju render`, also `python -m cuju.render`.
5
+
6
+ `cuju render` serves the page and opens Home, `cuju render open` opens a recording or a live
7
+ source, `cuju render app` opens the page in a window of its own and with `--install` adds it to
8
+ the computer's apps, `cuju render team add FOLDER` registers a Kaggle submission folder,
9
+ `cuju render play` records a match between two registered teams, `cuju render challenge` lists
10
+ the challenges, plays one headless with a team or benchmarks several teams on several challenges,
11
+ `cuju render tournament` makes, lists, shows, runs
12
+ and cancels tournaments, and `cuju render sample` adds the shipped sample matches and the starter
13
+ team again, or brings them back from the Trash. `cuju render recordings` lists the
14
+ library with the group of each challenge run, `cuju render search` finds moments across the
15
+ library, and `cuju render captions` writes the captions of a recording as WebVTT. `cuju render demo`
16
+ streams a live match between the built-in AIs to the page, and `cuju render video` exports a range
17
+ of a recording as an MP4 or WebM video, drawn and encoded by the page in a headless Chrome, Edge or
18
+ Chromium.
19
+ `cuju render tutorial` prints the lessons of the page for an agent and checks each one from the
20
+ home folder, `cuju render doctor` prints a diagnostic report, and `cuju render feed` prints the
21
+ addresses of clean feeds, pictures of fixed size for a streaming program such as OBS.
22
+ `cuju render pack check PATH` checks a team's presentation package as the server and the page
23
+ read it, `cuju render pack new FOLDER` writes a small working team with a package to start from,
24
+ `cuju render pack preview PATH` opens a package on a sample match with the preview panel, and
25
+ `cuju render pack sides` shows or sets the registered team whose presentation each side draws.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import json
32
+ import os
33
+ import sys
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ from cuju.errors import Error
38
+
39
+ from ._store import home as _home
40
+ from ._text.messages import counted
41
+
42
+
43
+ def main(arguments: list[str] | None = None) -> int:
44
+ """Runs the command that the arguments name and returns its exit status."""
45
+ parser = argparse.ArgumentParser(
46
+ prog='cuju render', description="Watch cuju's matches in a browser."
47
+ )
48
+ parser.add_argument(
49
+ '--home',
50
+ help=f'the renderer folder, by default the folder of ${_home.HOME_VARIABLE} when set, '
51
+ 'else ~/.cuju/render',
52
+ )
53
+ commands = parser.add_subparsers(dest='command')
54
+ server = commands.add_parser(
55
+ 'serve', help='serve the page, which a bare cuju render also opens'
56
+ )
57
+ server.add_argument('--port', type=int, default=8780, help='the port, 8780 by default')
58
+ server.add_argument('--open', action='store_true', help='open the page in the browser')
59
+ server.add_argument(
60
+ '--remote',
61
+ action='store_true',
62
+ help='print the SSH command that reaches the page from another computer',
63
+ )
64
+ # A server that `cuju.render.show` starts ends with the process that started it.
65
+ server.add_argument('--parent', type=int, default=None, help=argparse.SUPPRESS)
66
+ server.add_argument('--colab', action='store_true', help=argparse.SUPPRESS)
67
+ opener = commands.add_parser(
68
+ 'open', help='open a recording file, a recording or live source, or a URL of the page'
69
+ )
70
+ opener.add_argument('target', help='a .cjr file, a recording or live source identifier, a URL')
71
+ opener.add_argument(
72
+ '--port',
73
+ type=int,
74
+ default=8780,
75
+ help='the port of a server that it starts, 8780 by default',
76
+ )
77
+ _app_parser(commands)
78
+ team = commands.add_parser(
79
+ 'team', help='add, list, show, set, export, remove or give sounds to teams of the registry'
80
+ )
81
+ team.add_argument('action', choices=('add', 'list', 'show', 'set', 'export', 'remove', 'sound'))
82
+ team.add_argument(
83
+ 'target', nargs='?', help='a submission folder or archive to add, or a team identifier'
84
+ )
85
+ team.add_argument(
86
+ 'sound',
87
+ nargs='*',
88
+ metavar='KIND [FILE]',
89
+ help='for team sound, the moment, goal, shot, pass, tackle, save, kick, celebration or '
90
+ 'chant, and the Ogg, MP3 or WAV file of at most 2 MB that it plays',
91
+ )
92
+ team.add_argument(
93
+ '--remove', action='store_true', help='for team sound, remove the sound of the moment'
94
+ )
95
+ team.add_argument(
96
+ '--name', help='the name of the team, by default the folder name of an added one'
97
+ )
98
+ team.add_argument(
99
+ '--format',
100
+ choices=('5v5', '11v11'),
101
+ default=None,
102
+ help='the size of the team, 11v11 by default for an added team, unless its folder holds '
103
+ 'an export',
104
+ )
105
+ _team_control_arguments(team)
106
+ team.add_argument('--out', help='the zip file of an export, by default the team name')
107
+ team.add_argument(
108
+ '--json', action='store_true', help='print the team, or the list of teams, as JSON'
109
+ )
110
+ match = commands.add_parser('play', help='record a match between two registered teams')
111
+ for side in ('left', 'right'):
112
+ match.add_argument(
113
+ f'--{side}',
114
+ default='builtin',
115
+ help='a team identifier, builtin, or random for a team that the seed draws',
116
+ )
117
+ match.add_argument(
118
+ '--scenario',
119
+ default='11_vs_11_kaggle',
120
+ help='a shipped scenario, 11_vs_11_kaggle by default, or random for one that fits both',
121
+ )
122
+ match.add_argument(
123
+ '--pool',
124
+ default=None,
125
+ help='the teams that a random side draws from, as identifiers with commas, every team '
126
+ 'of the registry by default',
127
+ )
128
+ match.add_argument(
129
+ '--same-team',
130
+ action='store_true',
131
+ help='let one team draw both sides while another team fits',
132
+ )
133
+ match.add_argument('--seed', type=int, default=None, help='the seed, drawn when absent')
134
+ match.add_argument('--device', default='cpu', help='cpu or cuda, cpu by default')
135
+ for side in ('left', 'right'):
136
+ match.add_argument(
137
+ f'--{side}-ability',
138
+ type=float,
139
+ default=None,
140
+ help=f"the {side} built-in AI's ability from 0 to 1, the scenario's when absent",
141
+ )
142
+ match.add_argument('--json', action='store_true', help='print the saved recording as JSON')
143
+ _tournament_parser(commands)
144
+ challenge = commands.add_parser(
145
+ 'challenge',
146
+ help='list the challenges, play one headless with a registered team, or benchmark teams',
147
+ )
148
+ challenge.add_argument(
149
+ 'name', help='list, run, bench, or the challenge to play, such as empty_goal_close'
150
+ )
151
+ challenge.add_argument(
152
+ 'identifier', nargs='?', help='the challenge that run plays, such as empty_goal_close'
153
+ )
154
+ challenge.add_argument(
155
+ '--team',
156
+ action='append',
157
+ help='a team identifier, or builtin, given once for a run and once for each model of bench',
158
+ )
159
+ challenge.add_argument(
160
+ '--challenge',
161
+ action='append',
162
+ help='a challenge of bench, given once for each, every challenge when absent',
163
+ )
164
+ challenge.add_argument(
165
+ '--attempts', type=int, default=None, help='fewer attempts than the challenge is practice'
166
+ )
167
+ challenge.add_argument('--device', default='cpu', help='cpu or cuda, cpu by default')
168
+ challenge.add_argument(
169
+ '--no-recordings',
170
+ action='store_true',
171
+ help='score the run without keeping its attempts as recordings',
172
+ )
173
+ challenge.add_argument(
174
+ '--keep-recordings',
175
+ action='store_true',
176
+ help='keep the attempts of bench as recordings, which bench leaves out by default',
177
+ )
178
+ challenge.add_argument(
179
+ '--workers', type=int, default=None, help='the runs that bench plays at once, 1 to 8'
180
+ )
181
+ challenge.add_argument(
182
+ '--json',
183
+ action='store_true',
184
+ help='print the catalog, the result of a run or the benchmark as JSON',
185
+ )
186
+ challenge.add_argument('--csv', action='store_true', help='print the table of bench as CSV')
187
+ listing = commands.add_parser(
188
+ 'recordings', help='list the recordings with the group of each challenge run'
189
+ )
190
+ listing.add_argument('--group', help='list the recordings of one group alone')
191
+ listing.add_argument('--trash', action='store_true', help='list the Trash')
192
+ moves = listing.add_mutually_exclusive_group()
193
+ moves.add_argument(
194
+ '--delete-group', metavar='GROUP', help='move every recording of a group to the Trash'
195
+ )
196
+ moves.add_argument(
197
+ '--restore-group', metavar='GROUP', help='restore every recording of a group'
198
+ )
199
+ listing.add_argument('--json', action='store_true', help='print the recordings as JSON')
200
+ sample = commands.add_parser(
201
+ 'sample',
202
+ help='add the sample matches and the starter team again, or restore them from the Trash',
203
+ )
204
+ sample.add_argument(
205
+ 'name',
206
+ nargs='?',
207
+ choices=('11v11', '5v5', 'sample-11v11', 'sample-5v5', 'starter-team', 'list'),
208
+ help='one sample, by default every one, or list to show what the library holds of each',
209
+ )
210
+ sample.add_argument(
211
+ '--json', action='store_true', help='print the list, or the changes, as JSON'
212
+ )
213
+ tutorial = commands.add_parser(
214
+ 'tutorial', help='the lessons of the page for an agent, each step checked from the server'
215
+ )
216
+ tutorial.add_argument('lesson', nargs='?', help='one lesson, such as seek_goal, by default all')
217
+ tutorial.add_argument('--json', action='store_true', help='print the lessons as JSON')
218
+ tutorial.add_argument(
219
+ '--check', action='store_true', help='exit with status 2 until the lessons shown are done'
220
+ )
221
+ tutorial.add_argument(
222
+ '--reset', action='store_true', help='forget the lessons that pages reported done'
223
+ )
224
+ from ._guide import doctor as _doctor
225
+
226
+ doctor = commands.add_parser(
227
+ 'doctor', help='print a diagnostic report of versions, the home folder and the server'
228
+ )
229
+ _doctor.add_arguments(doctor)
230
+ show = commands.add_parser(
231
+ 'demo',
232
+ help='stream a live match between the built-in AIs',
233
+ description='Starts the server unless one answers on the port, streams a live match '
234
+ 'between the built-in AIs at real time and opens the page on it, until Ctrl+C.',
235
+ )
236
+ show.add_argument('--port', type=int, default=8780, help='the port, 8780 by default')
237
+ show.add_argument('--eleven', action='store_true', help='eleven a side, five by default')
238
+ show.add_argument('--scenario', help='another shipped scenario, such as academy_corner')
239
+ show.add_argument('--seed', type=int, default=None, help='the seed, drawn when absent')
240
+ show.add_argument('--no-open', action='store_true', help='leave the browser closed')
241
+ finder = commands.add_parser(
242
+ 'search',
243
+ help='find moments of every recording, such as goal team:Greens after:60',
244
+ description='Finds the moments of the library that a query names. ' + _QUERY_HELP,
245
+ )
246
+ finder.add_argument('query', nargs='*', help='words and filters, quoted as one argument or not')
247
+ finder.add_argument('--limit', type=int, default=None, help='the most results, 50 by default')
248
+ finder.add_argument('--json', action='store_true', help='print the results as JSON')
249
+ captions = commands.add_parser(
250
+ 'captions', help='write the captions of a recording as a WebVTT file'
251
+ )
252
+ captions.add_argument('recording', help='a recording identifier of the library or a .cjr file')
253
+ captions.add_argument('-o', '--out', help='the WebVTT file, by default printed')
254
+ captions.add_argument('--json', action='store_true', help='print the cues as JSON')
255
+ from ._export import video as _video
256
+
257
+ video = commands.add_parser(
258
+ 'video',
259
+ help='export a range of a recording as an MP4 or WebM video, with Chrome or Edge',
260
+ description='Draws a range of a recording on the page in a headless Chrome, Edge or '
261
+ 'Chromium, frame by frame, and encodes it in the browser, MP4 with H.264 where the browser '
262
+ 'encodes it and WebM otherwise. ffmpeg, when installed, encodes an MP4 that the browser '
263
+ 'cannot. The video is silent.',
264
+ )
265
+ _video.add_arguments(video)
266
+ from ._export import feed as _feed
267
+
268
+ feed = commands.add_parser(
269
+ 'feed',
270
+ help='print the address of a clean feed for OBS, or of every source a feed may follow',
271
+ description='Prints the address of the page at /feed, a picture of fixed size with the '
272
+ 'broadcast graphics alone, for a browser source of OBS or another streaming program.',
273
+ )
274
+ _feed.add_arguments(feed)
275
+ pack = commands.add_parser(
276
+ 'pack', help="check a team's presentation package, or write a new one to start from"
277
+ )
278
+ pack.add_argument('action', choices=('check', 'new', 'preview', 'sides'))
279
+ pack.add_argument(
280
+ 'target',
281
+ nargs='?',
282
+ help='a team folder, cuju-team folder, presentation folder or .zip to check or preview, '
283
+ 'or the empty folder of a new team',
284
+ )
285
+ pack.add_argument(
286
+ '--side', choices=('left', 'right'), default='left', help='the side a preview draws'
287
+ )
288
+ for side in ('left', 'right'):
289
+ pack.add_argument(
290
+ f'--{side}', help=f'the team whose presentation the {side} side draws, or none'
291
+ )
292
+ pack.add_argument('--port', type=int, default=8780, help='the port of a preview server')
293
+ pack.add_argument(
294
+ '--json',
295
+ action='store_true',
296
+ help='print the result of a check, or the address and team of a preview, as JSON',
297
+ )
298
+ pack.add_argument(
299
+ '--no-open', action='store_true', help='open no browser for a preview, print its address'
300
+ )
301
+ pack.add_argument(
302
+ '--name', default='My team', help='the name of a new team, by default My team'
303
+ )
304
+ from ._text import languages as _languages
305
+
306
+ _languages.add_arguments(
307
+ commands.add_parser(
308
+ 'language',
309
+ help='write, check, list, add or remove a language of the page',
310
+ description='Writes a new language file from English, checks a file against English, '
311
+ 'and lists, adds or removes the languages of the home folder.',
312
+ )
313
+ )
314
+ options = parser.parse_args(arguments)
315
+ as_json = bool(getattr(options, 'json', False))
316
+ if as_json:
317
+ # A command that fails under --json answers with JSON on standard output alone.
318
+ parser.error = _json_usage # type: ignore[method-assign]
319
+ if options.command == 'language' and options.action in ('check', 'new'):
320
+ return _languages.run(options)
321
+ if options.command == 'pack' and options.action in ('check', 'new'):
322
+ if not options.target:
323
+ parser.error(f'expected the path that pack {options.action} reads or writes')
324
+ return _pack(options)
325
+ if options.home:
326
+ os.environ[_home.HOME_VARIABLE] = str(Path(options.home).expanduser().resolve())
327
+ try:
328
+ from ._http.launch import check_home
329
+
330
+ check_home()
331
+ return _run(parser, options)
332
+ except (Error, OSError) as error:
333
+ return _failed(error, as_json)
334
+
335
+
336
+ def _app_parser(commands: Any) -> None:
337
+ """Adds `cuju render app`, the page in a window of its own, and its launchers."""
338
+ app = commands.add_parser(
339
+ 'app',
340
+ help="open the page in a window of its own, or add it to the computer's apps",
341
+ description='Opens the page in a window of its own, through the system web view of '
342
+ "pywebview, from the extra cuju-render[app], else a Chromium browser's app mode, else "
343
+ 'the default browser. A server of the home folder is reused, and one that this command '
344
+ 'starts ends with the window. --install adds a launcher to the Start menu and the desktop '
345
+ 'on Windows, the applications menu on Linux or ~/Applications on macOS, and --uninstall '
346
+ 'removes exactly what --install made.',
347
+ )
348
+ app.add_argument(
349
+ '--port', type=int, default=None, help='the port of a server that it starts, 8780 when free'
350
+ )
351
+ app.add_argument(
352
+ '--route',
353
+ choices=['auto', 'webview', 'chromium', 'browser'],
354
+ default='auto',
355
+ help='the system web view, a Chromium app window or the default browser, the first that '
356
+ 'the computer has by default',
357
+ )
358
+ app.add_argument('--browser', help='the Chromium browser for the route chromium, its program')
359
+ launcher = app.add_mutually_exclusive_group()
360
+ launcher.add_argument(
361
+ '--install', action='store_true', help="add a launcher of the app to the computer's apps"
362
+ )
363
+ launcher.add_argument(
364
+ '--uninstall', action='store_true', help='remove the launchers that --install made'
365
+ )
366
+
367
+
368
+ def _app(parser: argparse.ArgumentParser, options: argparse.Namespace) -> int:
369
+ """Runs `cuju render app`."""
370
+ if options.install or options.uninstall:
371
+ from ._http import launchers as _launchers
372
+
373
+ return _launchers.run(options.install, options.route, options.browser)
374
+ if options.port is not None and not 0 <= options.port <= 65535:
375
+ parser.error('expected a port from 0 to 65535')
376
+ from ._http import window as _window
377
+
378
+ return _window.run(options.port, options.route, options.browser)
379
+
380
+
381
+ def _failed(error: BaseException, as_json: bool) -> int:
382
+ """Prints the error of a command that failed and returns the exit status 1.
383
+
384
+ Under --json the error goes to standard output alone, as a JSON object of `error`, its text,
385
+ and `error_id` and `error_values`, the message that the error carries or null, so a script
386
+ reads one answer whatever happens. Otherwise a line goes to standard error.
387
+ """
388
+ if not as_json:
389
+ print(f'cuju render: {error}', file=sys.stderr)
390
+ return 1
391
+ from ._text import messages as _messages
392
+
393
+ # An error raised again with the same text keeps the message of its cause.
394
+ cause = error.__cause__
395
+ if getattr(error, 'message_id', None) is None and isinstance(cause, _messages.Message):
396
+ error, shown = cause, str(error)
397
+ else:
398
+ shown = str(error)
399
+ print(json.dumps(_messages.entry_fields(error, shown), indent=1))
400
+ return 1
401
+
402
+
403
+ def _json_usage(message: str) -> None:
404
+ """Ends a command whose arguments do not fit, under --json, with the error as JSON."""
405
+ print(json.dumps({'error': message, 'error_id': None, 'error_values': None}, indent=1))
406
+ sys.exit(2)
407
+
408
+
409
+ def _run(parser: argparse.ArgumentParser, options: argparse.Namespace) -> int:
410
+ """Runs one command of the parsed command line."""
411
+ if options.command == 'app':
412
+ return _app(parser, options)
413
+ if options.command == 'team':
414
+ from ._registry import teams as _teams
415
+
416
+ if options.action in ('show', 'set', 'export', 'remove', 'sound') and not options.target:
417
+ parser.error(f'team {options.action} needs the identifier of a team')
418
+ if options.action == 'add' and not options.target:
419
+ parser.error('team add needs a submission folder or archive')
420
+ control = _team_control(options)
421
+ if options.action == 'add':
422
+ entry = _teams.add_folder(
423
+ options.target, options.name, options.format or '11v11', control
424
+ )
425
+ _print_team(entry, options.json, 'added')
426
+ elif options.action == 'show':
427
+ _print_team(_teams.entry(options.target), options.json, None)
428
+ elif options.action == 'sound':
429
+ _team_sound(_teams, options)
430
+ elif options.action == 'set':
431
+ if control is not None:
432
+ control = _changed_control(_teams, options, control)
433
+ entry = _teams.update(options.target, options.name, None, options.format, control)
434
+ _print_team(entry, options.json, 'changed')
435
+ elif options.action == 'export':
436
+ filename, data = _teams.export(options.target)
437
+ path = options.out or filename
438
+ with open(path, 'wb') as stream:
439
+ stream.write(data)
440
+ print(f'exported {path}')
441
+ elif options.action == 'remove':
442
+ if not _teams.remove(options.target):
443
+ parser.error(f'no team {options.target!r} to remove')
444
+ print(f'removed {options.target}')
445
+ elif options.json:
446
+ print(json.dumps(_teams.teams(), indent=1))
447
+ else:
448
+ for entry in _teams.teams():
449
+ print(f'{entry["id"]} {entry["format"]} {entry["name"]}')
450
+ return 0
451
+ if options.command == 'sample':
452
+ from ._store import samples as _samples
453
+
454
+ if options.name == 'list':
455
+ if options.json:
456
+ print(json.dumps(_samples.listing(), indent=1))
457
+ return 0
458
+ for entry in _samples.listing():
459
+ print(f'{entry["id"]} {entry["kind"]} {entry["state"]} {entry["title"]}')
460
+ return 0
461
+ names = None
462
+ if options.name is not None:
463
+ names = [options.name if '-' in options.name else f'sample-{options.name}']
464
+ result = _samples.add(names)
465
+ if options.json:
466
+ print(json.dumps(result, indent=1))
467
+ return 0
468
+ for change in result['changes']:
469
+ target = next(e['target'] for e in result['samples'] if e['id'] == change['id'])
470
+ named = f' as {target}' if target != change['id'] else ''
471
+ print(f'{change["change"]} {change["id"]}{named}')
472
+ if not result['changes']:
473
+ print('the library already holds the samples')
474
+ return 0
475
+ if options.command == 'tutorial':
476
+ from ._guide import tutorial as _tutorial
477
+
478
+ return _tutorial.run(options.lesson, options.json, options.reset, options.check)
479
+ if options.command == 'doctor':
480
+ from ._guide import doctor as _doctor
481
+
482
+ return _doctor.run(options)
483
+ if options.command == 'search':
484
+ return _find(options)
485
+ if options.command == 'captions':
486
+ return _write_captions(options)
487
+ if options.command == 'language':
488
+ from ._text import languages as _languages
489
+
490
+ return _languages.run(options)
491
+ if options.command == 'tournament':
492
+ from ._tournaments.tournament_cli import run_command
493
+
494
+ return run_command(parser, options)
495
+ if options.command == 'video':
496
+ from ._export import video as _video
497
+
498
+ return _video.run(options)
499
+ if options.command == 'feed':
500
+ from ._export import feed as _feed
501
+
502
+ return _feed.run(options)
503
+ if options.command == 'challenge':
504
+ return _challenge(parser, options)
505
+ if options.command == 'recordings':
506
+ return _recordings(options)
507
+ if options.command == 'play':
508
+ return _play_match(options)
509
+ if options.command == 'demo':
510
+ from ._playing import demo as _demo
511
+
512
+ if not 0 <= options.port <= 65535:
513
+ parser.error('expected a port from 0 to 65535')
514
+ from cuju import bundled_scenarios
515
+
516
+ scenario = options.scenario or _demo.SCENARIOS['11v11' if options.eleven else '5v5']
517
+ if scenario not in bundled_scenarios():
518
+ parser.error(f'expected a shipped scenario, found {scenario!r}')
519
+ return _demo.demo(options.port, scenario, not options.no_open, options.seed)
520
+ port = getattr(options, 'port', 8780)
521
+ if not 0 <= port <= 65535:
522
+ parser.error('expected a port from 0 to 65535')
523
+ from ._http.commands import open_target, serve_page
524
+
525
+ if options.command == 'open':
526
+ return open_target(options.target, port)
527
+ if options.command == 'pack':
528
+ return _pack_home(options, port)
529
+ return serve_page(port, options)
530
+
531
+
532
+ def _team_sound(teams: Any, options: argparse.Namespace) -> None:
533
+ """Runs `cuju render team sound ID [KIND [FILE]]`, which lists, gives or removes sounds.
534
+
535
+ A sound is checked as the page's upload is, `PUT /api/teams/ID/sounds/KIND`.
536
+ """
537
+ from cuju.errors import ArgumentError
538
+
539
+ kind, *rest = options.sound or [None]
540
+ if kind is not None and kind not in teams.SOUND_SLOTS:
541
+ raise ArgumentError(
542
+ f'expected the moment of a sound among {", ".join(teams.SOUND_SLOTS)}, found {kind}'
543
+ )
544
+ if kind is not None and options.remove:
545
+ teams.entry(options.target)
546
+ teams.remove_sound(options.target, kind)
547
+ elif kind is not None and rest:
548
+ teams.set_sound(options.target, kind, Path(rest[0]).expanduser().read_bytes())
549
+ elif kind is not None:
550
+ raise ArgumentError('expected the sound file of the moment, or --remove')
551
+ entry = teams.entry(options.target)
552
+ if options.json:
553
+ print(json.dumps({'team': entry['id'], 'sounds': entry['sounds']}))
554
+ else:
555
+ print(f'{entry["name"]}, sounds {", ".join(entry["sounds"]) or "none"}')
556
+
557
+
558
+ def _team_control_arguments(team: Any) -> None:
559
+ """Adds the options of `cuju render team` that set the players a submission controls."""
560
+ team.add_argument(
561
+ '--players', type=int, help='the players that the submission controls, 1 by default'
562
+ )
563
+ team.add_argument(
564
+ '--assignment',
565
+ choices=('dynamic', 'fixed'),
566
+ help='dynamic for the players nearest the ball, fixed for those of --selection',
567
+ )
568
+ team.add_argument(
569
+ '--selection',
570
+ help='the fixed players, as indices of the formation or GRF roles, such as 1,2,3 or CB,CM',
571
+ )
572
+ team.add_argument(
573
+ '--goalkeeper',
574
+ choices=('agent', 'builtin', 'scenario'),
575
+ help='who plays the goalkeeper, the submission, the built-in AI or as the scenario says',
576
+ )
577
+ team.add_argument(
578
+ '--action-set',
579
+ choices=('default', 'v2'),
580
+ help="the submission's action set, GRF's default or v2",
581
+ )
582
+
583
+
584
+ def _team_control(options: argparse.Namespace) -> dict[str, Any] | None:
585
+ """Returns the control that the options of `cuju render team` name, or `None` for none."""
586
+ control: dict[str, Any] = {}
587
+ if options.players is not None:
588
+ control['players'] = options.players
589
+ if options.selection is not None:
590
+ parts = [part.strip() for part in options.selection.split(',') if part.strip()]
591
+ control['selection'] = [int(part) if part.isdigit() else part for part in parts]
592
+ control['assignment'] = 'fixed'
593
+ control.setdefault('players', len(parts))
594
+ if options.assignment is not None:
595
+ control['assignment'] = options.assignment
596
+ if options.goalkeeper is not None:
597
+ control['goalkeeper'] = options.goalkeeper
598
+ if options.action_set is not None:
599
+ control['action_set'] = options.action_set
600
+ return control or None
601
+
602
+
603
+ def _changed_control(teams: Any, options: argparse.Namespace, control: dict[str, Any]) -> Any:
604
+ """Returns a team's control with the changes of `cuju render team set` applied to it."""
605
+ current = teams.entry(options.target)
606
+ try:
607
+ known = teams.check_control(current.get('control'), options.format or current['format'])
608
+ except Error:
609
+ known = dict(teams.DEFAULT_CONTROL)
610
+ if control.get('assignment', known['assignment']) == 'dynamic':
611
+ # Dynamic assignment keeps no selection, and the goalkeeper goes back to the scenario.
612
+ known['selection'] = []
613
+ if known['goalkeeper'] == 'builtin':
614
+ known['goalkeeper'] = 'scenario'
615
+ return {**known, **control}
616
+
617
+
618
+ def _print_team(entry: dict[str, Any], as_json: bool, change: str | None) -> None:
619
+ """Prints a team of the registry, as JSON or as lines of its name, format and control."""
620
+ if as_json:
621
+ print(json.dumps(entry, indent=1))
622
+ return
623
+ head = f'{change} ' if change else ''
624
+ print(f'{head}{entry["id"]} {entry["name"]}')
625
+ control = entry.get('control')
626
+ if entry.get('kind') != 'builtin' and isinstance(control, dict):
627
+ selection = ','.join(str(item) for item in control.get('selection') or []) or 'none'
628
+ print(
629
+ f' format {entry["format"]}, {control["players"]} controlled, assignment '
630
+ f'{control["assignment"]}, selection {selection}, goalkeeper '
631
+ f'{control["goalkeeper"]}, action set {control["action_set"]}'
632
+ )
633
+
634
+
635
+ _QUERY_HELP = (
636
+ 'Words name kinds, such as goal, shot, save, pass, tackle, card, corner or penalty, and '
637
+ 'filters such as team:, player:, after:, before:, score:, source: and outcome: narrow them.'
638
+ )
639
+
640
+
641
+ def _find(options: argparse.Namespace) -> int:
642
+ """Prints the moments that a search finds, under the identifier and title of each recording,
643
+ or as JSON.
644
+
645
+ Each moment shows its clock, its time `t` in seconds of the recording, its line of text and the
646
+ whole address that opens Watch a few seconds before it, on the running server or, when none
647
+ runs, on the port that `cuju render serve` takes by default.
648
+ """
649
+ import json
650
+
651
+ from ._http import launch as _launch
652
+ from ._moments import search as _search
653
+
654
+ found = _search.search(' '.join(options.query), options.limit)
655
+ if options.json:
656
+ print(json.dumps(found, indent=1))
657
+ return 0
658
+ server = _launch.running()
659
+ base = server.url if server is not None else 'http://127.0.0.1:8780/'
660
+ match = None
661
+ for result in found['results']:
662
+ if result['match'] != match:
663
+ match = result['match']
664
+ print(f'{match} {result["title"]}')
665
+ print(f' {result["minute"]:>6} t={result["t"]:g} {result["text"]}')
666
+ print(f' {base}{result["url"]}')
667
+ shown = len(found['results'])
668
+ moments = counted(found['total'], 'moment')
669
+ print(f'{shown} of {moments} in {counted(found["recordings"], "recording")}')
670
+ if shown and server is None:
671
+ print('No server runs, and cuju render open with an address above in quotes starts one.')
672
+ return 0
673
+
674
+
675
+ def _write_captions(options: argparse.Namespace) -> int:
676
+ """Writes or prints the captions of a recording."""
677
+ import json
678
+
679
+ from ._moments import search as _search
680
+
681
+ if options.json:
682
+ text = json.dumps(_search.captions(options.recording), indent=1)
683
+ else:
684
+ text = _search.captions_vtt(options.recording)
685
+ if options.out:
686
+ Path(options.out).write_text(text, encoding='utf-8', newline='\n')
687
+ print(f'wrote {options.out}')
688
+ else:
689
+ print(text)
690
+ return 0
691
+
692
+
693
+ def _pack_home(options: argparse.Namespace, port: int) -> int:
694
+ """Runs `cuju render pack preview` and `cuju render pack sides`, which use the home."""
695
+ import json
696
+
697
+ from ._packages import pack
698
+ from ._packages import presentation as _presentation
699
+
700
+ try:
701
+ if options.action == 'sides':
702
+ changes = {
703
+ side: (None if value == 'none' else value)
704
+ for side in ('left', 'right')
705
+ if (value := getattr(options, side)) is not None
706
+ }
707
+ sides = _presentation.assign(changes) if changes else _presentation.sides()
708
+ print(json.dumps(sides) if options.json else '\n'.join(pack.sides_lines(sides)))
709
+ return 0
710
+ if not options.target:
711
+ return _failed(Error('expected the package to preview'), options.json)
712
+ return pack.run_preview(
713
+ options.target, options.side, port, not options.no_open, options.json
714
+ )
715
+ except (Error, OSError) as error:
716
+ return _failed(error, options.json)
717
+
718
+
719
+ def _pack(options: argparse.Namespace) -> int:
720
+ """Checks a presentation package or writes a new team, and returns the exit status.
721
+
722
+ A check exits with 0 when the package holds no error and 1 otherwise. Both run without the
723
+ renderer's home, which they never read.
724
+ """
725
+ import json
726
+
727
+ from ._packages import pack
728
+
729
+ try:
730
+ if options.action == 'new':
731
+ for path in pack.new(options.target, options.name):
732
+ print(f'wrote {path}')
733
+ print('\n'.join(pack.next_steps(options.target)))
734
+ return 0
735
+ result = pack.check(options.target)
736
+ except (Error, OSError) as error:
737
+ return _failed(error, options.json)
738
+ if options.json:
739
+ print(json.dumps(result, indent=2))
740
+ else:
741
+ print(pack.text_report(result, options.target))
742
+ return 0 if result['ok'] else 1
743
+
744
+
745
+ def _tournament_parser(commands: Any) -> None:
746
+ """Adds `cuju render tournament` with its actions and options."""
747
+
748
+ class Entrant(argparse.Action):
749
+ """Keeps the entrants of every kind in the order the command line names them.
750
+
751
+ `--as` names the entrant just before it, so a team entered twice reads apart, and
752
+ `--ability` gives it an ability of its own, or sets the standard before every entrant.
753
+ """
754
+
755
+ def __call__(self, parser: Any, namespace: Any, value: Any, flag: Any = None) -> None:
756
+ kinds = {
757
+ '--team': 'team',
758
+ '--builtin': 'builtin',
759
+ '--person': 'person',
760
+ '--random': 'random',
761
+ '--as': 'as',
762
+ '--ability': 'ability',
763
+ }
764
+ kind = kinds[flag]
765
+ entrants = getattr(namespace, 'entrants', None) or []
766
+ entrants.append((kind, value))
767
+ namespace.entrants = entrants
768
+
769
+ cup = commands.add_parser(
770
+ 'tournament', help='make, check, list, show, run, change or delete tournaments'
771
+ )
772
+ cup.add_argument(
773
+ 'action',
774
+ choices=(
775
+ 'new',
776
+ 'check',
777
+ 'list',
778
+ 'show',
779
+ 'run',
780
+ 'cancel',
781
+ 'delete',
782
+ 'presets',
783
+ 'ratings',
784
+ 'forfeit',
785
+ 'award',
786
+ 'resume',
787
+ 'preflight',
788
+ 'export',
789
+ 'legs',
790
+ 'replay',
791
+ ),
792
+ )
793
+ cup.add_argument('target', nargs='?', help='the identifier of a tournament')
794
+ cup.add_argument('--preset', help='a preset of formats, quick_cup when neither it nor --config')
795
+ cup.add_argument('--config', help='a JSON file with the whole configuration of a tournament')
796
+ cup.add_argument('--name', help='the name of the tournament')
797
+ cup.add_argument('--seed', type=int, help='the tournament seed, drawn when absent')
798
+ cup.add_argument('--team', action=Entrant, dest='entrants', help='a registered team')
799
+ cup.add_argument(
800
+ '--builtin', action=Entrant, dest='entrants', help='the built-in AI at an ability, or any'
801
+ )
802
+ cup.add_argument('--person', action=Entrant, dest='entrants', help='a person, by name')
803
+ cup.add_argument(
804
+ '--as',
805
+ action=Entrant,
806
+ dest='entrants',
807
+ metavar='NAME',
808
+ help='the name of the entrant just before it, such as a team entered twice',
809
+ )
810
+ cup.add_argument(
811
+ '--ability',
812
+ action=Entrant,
813
+ dest='entrants',
814
+ metavar='ABILITY',
815
+ help='before every entrant the standard ability from 0 to 1 or scenario, '
816
+ 'after an entrant its own ability or standard',
817
+ )
818
+ cup.add_argument('--seeding', choices=('random', 'manual', 'rating'))
819
+ cup.add_argument('--scenario', help='the scenario of every stage')
820
+ cup.add_argument('--length', type=int, help='the steps of every match')
821
+ cup.add_argument('--concurrency', type=int, help='the matches of agents at once, 1 by default')
822
+ cup.add_argument('--run', action='store_true', help='run the new tournament at once')
823
+ cup.add_argument('--rounds', type=int, help='the rounds of every Swiss or ladder stage')
824
+ cup.add_argument('--match', help='the key of the match that forfeit or award gives')
825
+ cup.add_argument(
826
+ '--winner', choices=('home', 'away'), help='the side that forfeit or award gives it to'
827
+ )
828
+ cup.add_argument(
829
+ '--random',
830
+ action=Entrant,
831
+ dest='entrants',
832
+ nargs=0,
833
+ help='the random entrant, one player with actions drawn at random',
834
+ )
835
+ cup.add_argument(
836
+ '--strict',
837
+ nargs='?',
838
+ const='{}',
839
+ default=None,
840
+ metavar='LIMITS',
841
+ help='strict rules, with the default limits or a JSON object of some of them',
842
+ )
843
+ cup.add_argument(
844
+ '--preflight', action='store_true', help='check every team before the first match'
845
+ )
846
+ cup.add_argument('--to', help='the folder that export writes the files of results to')
847
+ cup.add_argument('--legs', type=int, help='the legs that legs adds to the last stage')
848
+ cup.add_argument('--json', action='store_true', help='print what the action prints as JSON')
849
+
850
+
851
+ def _progress_line() -> Any:
852
+ """Returns a function that shows the progress of a match on one line of a terminal.
853
+
854
+ The line goes to standard error and only to a terminal, so the output of a script or a pipe
855
+ holds the result alone.
856
+ """
857
+ if not sys.stderr.isatty():
858
+ return None
859
+
860
+ def show(share: float) -> None:
861
+ print(f'\rplaying, {share:.0%}', end='', file=sys.stderr, flush=True)
862
+
863
+ return show
864
+
865
+
866
+ def _play_match(options: argparse.Namespace) -> int:
867
+ """Plays a match between two registered teams and prints the recording it saved.
868
+
869
+ What the submissions print goes to standard error, so standard output holds the result alone.
870
+ """
871
+ import contextlib
872
+ import json
873
+ import secrets
874
+
875
+ from ._playing import draw as _draw
876
+ from ._playing.play import play
877
+
878
+ left, right, scenario, seed = options.left, options.right, options.scenario, options.seed
879
+ pool = None
880
+ if options.pool is not None:
881
+ pool = [item.strip() for item in options.pool.split(',') if item.strip()]
882
+ if _draw.RANDOM in (left, right, scenario):
883
+ # The draw follows the seed, so a seed of its own draws the same teams again.
884
+ seed = secrets.randbelow(2**31) if seed is None else seed
885
+ drawn = _draw.draw_request(left, right, scenario, seed, pool, options.same_team)
886
+ left, right, scenario = drawn['left'], drawn['right'], drawn['scenario']
887
+ draw = drawn['draw']
888
+ else:
889
+ draw = None
890
+ if draw is not None and not options.json:
891
+ print(f'drew {left} on the left and {right} on the right in {scenario}, seed {seed}')
892
+ progress = None if options.json else _progress_line()
893
+ try:
894
+ with contextlib.redirect_stdout(sys.stderr):
895
+ path = play(
896
+ left,
897
+ right,
898
+ scenario,
899
+ seed,
900
+ None,
901
+ options.device,
902
+ (options.left_ability, options.right_ability),
903
+ progress=progress,
904
+ policy_errors=True,
905
+ draw=draw,
906
+ )
907
+ finally:
908
+ if progress is not None:
909
+ print('\r\033[K', end='', file=sys.stderr, flush=True)
910
+ if options.json:
911
+ answer = {'match': path.stem, 'path': str(path)}
912
+ if draw is not None:
913
+ answer.update(left=left, right=right, scenario=scenario, seed=seed, draw=draw)
914
+ print(json.dumps(answer))
915
+ else:
916
+ print(f'saved {path}')
917
+ return 0
918
+
919
+
920
+ def _challenge(parser: argparse.ArgumentParser, options: argparse.Namespace) -> int:
921
+ """Lists the challenges, plays one headless and prints each attempt and the result, or plays
922
+ a benchmark of several teams and prints its table."""
923
+ import contextlib
924
+ import json
925
+
926
+ from ._playing import challenges as _challenges
927
+
928
+ if options.name == 'list' and options.json:
929
+ print(json.dumps(_challenges.challenges(), indent=1))
930
+ return 0
931
+ if options.name == 'list':
932
+ for entry in _challenges.challenges():
933
+ targets = ', '.join(
934
+ f'{medal} {target}'
935
+ for medal, target in zip(_challenges.MEDALS, entry['targets'], strict=True)
936
+ )
937
+ attempts = counted(entry['attempts'], 'attempt')
938
+ print(f'{entry["id"]} {entry["title"]}, {attempts}, {targets}')
939
+ print(f' {entry["goal"]}')
940
+ print(f' {_challenges.medal_text(entry)}')
941
+ return 0
942
+ if options.name == 'bench':
943
+ from cuju.errors import ArgumentError
944
+
945
+ from ._playing import bench as _bench
946
+
947
+ if not options.team:
948
+ parser.error('challenge bench needs --team, once for each team, or builtin')
949
+ try:
950
+ return _bench.command(options)
951
+ except ArgumentError as error:
952
+ return _failed(error, options.json)
953
+ identifier = options.name
954
+ if options.name == 'run':
955
+ if not options.identifier:
956
+ parser.error('challenge run needs the challenge to play, such as empty_goal_close')
957
+ identifier = options.identifier
958
+ elif options.identifier:
959
+ parser.error('a challenge plays one challenge, so name it alone or after run')
960
+ if not options.team or len(options.team) != 1:
961
+ parser.error('challenge needs one --team, a team identifier or builtin')
962
+ team = options.team[0]
963
+ from ._playing.play import PolicyError
964
+
965
+ spec = _challenges.find(identifier)
966
+
967
+ def show(result: dict) -> None:
968
+ if result['scored']:
969
+ seconds = f'scored after {counted(result["seconds"], "second")}'
970
+ if not result['counts'] and spec['limit'] is not None:
971
+ seconds += f', past the limit of {counted(spec["limit"], "second")}'
972
+ else:
973
+ seconds = 'no goal'
974
+ if result.get('failed'):
975
+ seconds = f'the policy failed, {result["error"]}, {seconds}'
976
+ goals = f'{result["goals"][0]} for, {result["goals"][1]} against'
977
+ kept = f'recording {result["match"]}' if result['match'] else 'no recording kept'
978
+ print(
979
+ f'attempt {result["index"] + 1}, seed {result["seed"]}, {seconds}, {goals}, {kept}',
980
+ flush=True,
981
+ )
982
+
983
+ try:
984
+ # What a team prints goes to standard error, so standard output holds the result alone.
985
+ with contextlib.redirect_stdout(sys.stderr if options.json else sys.stdout):
986
+ outcome = _challenges.run(
987
+ identifier,
988
+ team=team,
989
+ attempts=options.attempts,
990
+ device=options.device,
991
+ progress=None if options.json else show,
992
+ keep_recordings=not options.no_recordings,
993
+ fail_attempts=True,
994
+ )
995
+ except PolicyError as error:
996
+ return _failed(error, options.json)
997
+ if options.json:
998
+ print(json.dumps(outcome, indent=1))
999
+ return 0
1000
+ if any(result['counts'] for result in outcome['attempts']):
1001
+ print(f'value {outcome["value"]:g}, {counted(outcome["seconds"], "second")} to score')
1002
+ elif spec['measure'] == 'margin':
1003
+ print(f'value {outcome["value"]:g}, no goal scored')
1004
+ else:
1005
+ print(f'value {outcome["value"]:g}, no attempt counts towards it')
1006
+ print(_challenges.medal_text(spec, None if outcome['practice'] else outcome['value']))
1007
+ if outcome.get('playing'):
1008
+ played, controlled = outcome['playing']
1009
+ print(f'played with {played} of its {controlled} players, the others out')
1010
+ if outcome['practice']:
1011
+ print('practice, fewer attempts than the challenge, so no medal and no leaderboard')
1012
+ else:
1013
+ print(f'medal {outcome["medal"] or "none"}, rank {outcome["rank"] or "outside the board"}')
1014
+ best = outcome['personal_best']
1015
+ print(f'personal best {best["value"]:g} in {counted(best["seconds"], "second")}')
1016
+ if outcome['group']:
1017
+ print(f'group {outcome["group"]}, list it with cuju render recordings --group')
1018
+ return 0
1019
+
1020
+
1021
+ def _recordings(options: argparse.Namespace) -> int:
1022
+ """Lists the library by group, or one group, or moves a group to the Trash or back."""
1023
+ from ._store import library as _library
1024
+
1025
+ moved = options.delete_group or options.restore_group
1026
+ if moved:
1027
+ restore = bool(options.restore_group)
1028
+ changed = _library.trash_group(moved, restore=restore)
1029
+ if options.json:
1030
+ print(json.dumps({'group': moved, 'restored' if restore else 'trashed': changed}))
1031
+ elif changed:
1032
+ where = 'from the Trash' if restore else 'to the Trash'
1033
+ print(f'moved {counted(len(changed), "recording")} of {moved} {where}')
1034
+ if not changed:
1035
+ place = 'the Trash' if restore else 'the library'
1036
+ print(f'cuju render: no group {moved} in {place}', file=sys.stderr)
1037
+ return 1
1038
+ return 0
1039
+ entries = _library.matches(options.trash, options.group)
1040
+ if options.json:
1041
+ print(json.dumps(entries, indent=1))
1042
+ return 0
1043
+ if options.group and not entries:
1044
+ print(f'cuju render: no group {options.group}', file=sys.stderr)
1045
+ return 1
1046
+ listed = set()
1047
+ for entry in entries:
1048
+ group = entry.get('group')
1049
+ if group is None:
1050
+ print(f'{entry["id"]} {entry["title"]}')
1051
+ continue
1052
+ if group['id'] in listed:
1053
+ continue
1054
+ listed.add(group['id'])
1055
+ members = [item for item in entries if (item.get('group') or {}).get('id') == group['id']]
1056
+ members.sort(key=lambda item: item['group'].get('attempt', 0))
1057
+ shown = counted(len(members), 'recording')
1058
+ print(f'{group["id"]} {group["title"]}, {group["result"]}, {shown}')
1059
+ for member in members:
1060
+ print(f' {member["id"]} attempt {member["group"].get("attempt", 0) + 1}')
1061
+ return 0
1062
+
1063
+
1064
+ if __name__ == '__main__':
1065
+ sys.exit(main())