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,926 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Plays one match between two teams of the registry and saves it.
5
+
6
+ The server runs this module in a process of its own for each requested match, so a submission that
7
+ fails ends only that process, and the server ends one that runs past its time. Progress goes to the
8
+ server as one JSON line at a time on a channel of its own, while whatever a submission prints goes
9
+ to standard error, so no submission can forge a report. A match played in the caller's process,
10
+ from `cuju render play` or `cuju.render.play`, reports progress only to a function it is given.
11
+
12
+ A policy that fails raises `PolicyError` with a one-line reason. The variable `CUJU_RENDER_DEBUG`,
13
+ set to `on`, also prints the full traceback of the policy's error to standard error.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import collections
20
+ import hashlib
21
+ import json
22
+ import logging
23
+ import os
24
+ import queue
25
+ import random
26
+ import re
27
+ import struct
28
+ import sys
29
+ import threading
30
+ import time
31
+ import traceback
32
+ from collections.abc import Callable
33
+ from pathlib import Path
34
+ from typing import Any, TextIO
35
+
36
+ import torch
37
+
38
+ from cuju import BatchEnvironment, bundled_scenarios
39
+ from cuju.kaggle import (
40
+ AgentRunner,
41
+ accepts_configuration,
42
+ action_index,
43
+ action_indices,
44
+ observation,
45
+ )
46
+
47
+ from .._format import recording as _recording
48
+ from .._registry import teams as _teams
49
+ from .._store import library as _library
50
+ from .._text import messages as _messages
51
+ from .._text.messages import MessageError
52
+ from . import achievements as _achievements
53
+ from .errors import PolicyError
54
+
55
+ # The channel of the reports, which `main` opens on standard output once it has moved the
56
+ # submissions' prints away, and `None` for a match played in the caller's process.
57
+ _reports: TextIO | None = None
58
+ # The variable that prints the traceback of a policy's error, as `on`.
59
+ DEBUG_VARIABLE = 'CUJU_RENDER_DEBUG'
60
+ # GRF: a full match of the shipped scenarios lasts 3000 steps, so it takes 3001 frames, which the
61
+ # progress of a match divides by when its scenario names no duration.
62
+ _FRAMES = 3001
63
+ # The v2 action that hands a player to the builtin AI for one step.
64
+ _BUILTIN_AI = 19
65
+ # The actions of the default set.
66
+ DEFAULT_ACTIONS = 19
67
+ # The score of the left and the right side, at its place in a frame of a recording.
68
+ _SCORE = struct.Struct('<2i')
69
+
70
+
71
+ def _report(**entries: Any) -> None:
72
+ """Writes one line of progress for the server, when a server reads them."""
73
+ if _reports is None:
74
+ return
75
+ _reports.write(json.dumps(entries) + '\n')
76
+ _reports.flush()
77
+
78
+
79
+ def debugging() -> bool:
80
+ """Returns whether `CUJU_RENDER_DEBUG` asks for the tracebacks of policy errors."""
81
+ return os.environ.get(DEBUG_VARIABLE, '').strip().lower() not in ('', '0', 'off', 'no')
82
+
83
+
84
+ def _action_values(error: BaseException) -> dict[str, str] | None:
85
+ """Returns the answer and action set of an error for an action outside the set, or `None`."""
86
+ found = re.fullmatch(r'expected an action from -(\d+) to (\d+), found (.+)', str(error))
87
+ if isinstance(error, IndexError) and found:
88
+ count, last, value = found.groups()
89
+ return {'value': value, 'count': count, 'last': last}
90
+ return None
91
+
92
+
93
+ def _reason(error: BaseException) -> str:
94
+ """Returns the reason of a policy's error in one line, with the action set in words."""
95
+ values = _action_values(error)
96
+ if values is not None:
97
+ return (
98
+ f'it answered the action {values["value"]}, outside its action set of '
99
+ f'{values["count"]} actions, 0 to {values["last"]}'
100
+ )
101
+ return f'{type(error).__name__}: {error}'
102
+
103
+
104
+ def _policy_error(error: BaseException, player: str | None = None) -> PolicyError:
105
+ """Returns the `PolicyError` of a policy's error, which names the player when it is given.
106
+
107
+ Its text is that of `_reason` after the policy, and an action outside the set has a message
108
+ of its own, so the page shows that reason in its own language too.
109
+ """
110
+ named = {} if player is None else {'player': player}
111
+ message_id = 'server.policy.error' if player is None else 'server.policy.failed'
112
+ values = _action_values(error)
113
+ if values is not None:
114
+ return PolicyMessageError(f'{message_id}_action', **named, **values)
115
+ return PolicyMessageError(message_id, **named, reason=_reason(error))
116
+
117
+
118
+ def game_title(names: list[Any], tournament: dict[str, Any] | None) -> dict[str, Any]:
119
+ """Returns the title of a match as a label, its two sides by name, after its tournament match.
120
+
121
+ Args:
122
+ names: The name of each side, text or a label.
123
+ tournament: What the recording keeps of its tournament, whose name and match label come
124
+ before the sides, or `None`.
125
+ """
126
+ if tournament is not None:
127
+ from .._tournaments.tournament import match_title
128
+
129
+ return match_title(tournament, names[0], names[1])
130
+ return _messages.label('server.names.against', left=names[0], right=names[1])
131
+
132
+
133
+ def side_names(tournament: dict[str, Any] | None) -> list[Any]:
134
+ """Returns the name of each side that a tournament gives, a label where its rules made one,
135
+ or `None` for each side outside a tournament."""
136
+ tournament = tournament or {}
137
+ labels = tournament.get('labels')
138
+ if isinstance(labels, list) and len(labels) == 2:
139
+ return labels
140
+ return tournament.get('names') or [None, None]
141
+
142
+
143
+ def play(
144
+ left: str,
145
+ right: str,
146
+ scenario: str = '11_vs_11_kaggle',
147
+ seed: int | None = None,
148
+ title: str | dict[str, Any] | None = None,
149
+ device: str = 'cpu',
150
+ ability: tuple[float | None, float | None] = (None, None),
151
+ length: int | None = None,
152
+ tournament: dict[str, Any] | None = None,
153
+ progress: Callable[[float], None] | None = None,
154
+ policy_errors: bool = False,
155
+ draw: dict[str, Any] | None = None,
156
+ strict: dict[str, Any] | None = None,
157
+ stats: dict[str, Any] | None = None,
158
+ steps: int | None = None,
159
+ save: bool = True,
160
+ replay: dict[str, Any] | None = None,
161
+ ) -> Path | None:
162
+ """Plays a match between two teams of the registry, saves it and returns the recording.
163
+
164
+ Args:
165
+ left: The team of the left side, a registry identifier or `builtin`.
166
+ right: The team of the right side.
167
+ scenario: A shipped scenario, without agent counts, which the teams decide.
168
+ seed: The seed of the match, or `None` for one drawn from the clock.
169
+ title: The title of the recording, text or a label of `_messages.label`, by default the
170
+ two sides by name, after the tournament match.
171
+ device: `cpu` or `cuda`.
172
+ ability: The ability of the built-in AI on the left and on the right, each from 0 to 1, or
173
+ `None` for the scenario's. It is GRF's team difficulty, which steers every player that
174
+ the built-in AI controls, the whole team of the built-in AI and the teammates of a
175
+ submission's player alike.
176
+ length: The steps the match lasts, or `None` for the scenario's own duration.
177
+ tournament: The tournament, stage and match that the match belongs to, which its
178
+ provenance keeps, or `None`.
179
+ progress: A function called with the share of the match played, from 0 to 1, every 50
180
+ steps, or `None`.
181
+ policy_errors: Whether a submission that fails raises `PolicyError`, which names the team
182
+ and the reason in one line, in place of the submission's own error.
183
+ draw: What a request drew at random, as `draw.resolve` returns it under `draw`, which the
184
+ provenance keeps, or `None`.
185
+ strict: The limits of strict rules, as `tournament.check_strict` gives them, or `None`.
186
+ Under strict rules each submission plays in a process of its own within the limits,
187
+ and a side that breaks one ends the match at once, which raises `ForfeitError`.
188
+ stats: A dictionary that the match fills with its steps and, for each side, the calls of
189
+ its policy, their compute times and the actions it answered, or `None`.
190
+ steps: The steps after which the match stops, or `None` to play it to its end.
191
+ save: Whether to save the recording, which a check of a team leaves out.
192
+ replay: A dictionary that the match fills with what replays it exactly, its seed, the
193
+ settings of its batch and the actions of every step, or `None`.
194
+
195
+ Returns:
196
+ The path of the recording in the renderer's folder, or `None` when `save` is false.
197
+
198
+ Raises:
199
+ ArgumentError: If a team is unknown, plays another format than the scenario's, or an
200
+ ability lies outside 0 to 1.
201
+ PolicyError: If a submission fails and `policy_errors` asks for it.
202
+ ForfeitError: If a side breaks a rule under strict rules.
203
+ """
204
+ ability = _teams.check_ability(ability)
205
+ if scenario not in [*bundled_scenarios(), *_teams.renderer_scenarios()]:
206
+ raise MessageError('server.play.scenario', scenario=repr(scenario))
207
+ sides = [_teams.side_entry(left), _teams.side_entry(right)]
208
+ _teams.check_scenario(sides, scenario)
209
+ agents = [_teams.agent_count(team, scenario, side) for side, team in enumerate(sides)]
210
+ controls = [_teams.check_control(team.get('control'), team['format']) for team in sides]
211
+ extended = any(
212
+ team['kind'] != 'builtin' and (c['assignment'] == 'fixed' or c['action_set'] == 'v2')
213
+ for team, c in zip(sides, controls, strict=True)
214
+ )
215
+ seed = int(time.time()) % 2**31 if seed is None else int(seed)
216
+ if length is not None:
217
+ from .._tournaments.tournament_runner import length_scenario
218
+
219
+ asset = length_scenario(scenario, length)
220
+ elif scenario in _teams.renderer_scenarios():
221
+ asset = str(_teams.scenario_path(scenario))
222
+ else:
223
+ asset = f'{scenario}_l{agents[0]}_r{agents[1]}'
224
+ batch = BatchEnvironment(
225
+ asset,
226
+ agent_counts=[tuple(agents)],
227
+ environment_count=1,
228
+ representation='raw',
229
+ seed=seed,
230
+ auto_reset=False,
231
+ device=device,
232
+ difficulty=None if ability == (None, None) else [ability],
233
+ # GRF uses builtin AI for each player outside a fixed selection.
234
+ action_set='v2' if extended else 'default',
235
+ )
236
+ try:
237
+ reader = _recording.StateReader(int(batch.left_players[0] + batch.right_players[0]))
238
+ batch.reset()
239
+ state = batch.get_state(0)
240
+ frames = [reader.frame(state)]
241
+ # A failure names the side by its entrant, so two entrants of one team stay apart.
242
+ arguments = (
243
+ batch,
244
+ sides,
245
+ scenario,
246
+ agents,
247
+ extended,
248
+ [0, agents[0]],
249
+ side_names(tournament),
250
+ )
251
+ policies: Policies | None = None
252
+ done = False
253
+ stepped: list[list[int]] = []
254
+ if replay is not None:
255
+ replay.update(
256
+ seed=seed,
257
+ scenario=scenario,
258
+ asset=asset,
259
+ agents=list(agents),
260
+ action_set='v2' if extended else 'default',
261
+ ability=list(ability),
262
+ actions=stepped,
263
+ )
264
+ try:
265
+ if strict is not None:
266
+ from . import strict as _strict
267
+
268
+ policies = _strict.StrictPolicies(*arguments, seed=seed, limits=strict)
269
+ else:
270
+ policies = Policies(*arguments, seed=seed)
271
+ lineup = reader.lineup(state)
272
+ timing = _recording.scenario_timing(state)
273
+ # A match takes one frame more than its steps, the scenario's own or those of
274
+ # `length`.
275
+ total = timing['duration'] + 1 if timing['duration'] > 0 else _FRAMES
276
+ while not done and (steps is None or len(frames) <= steps):
277
+ parts = policies.observe()
278
+ try:
279
+ answers = policies.decide(parts)
280
+ except RuleError:
281
+ raise
282
+ except PolicyError as error:
283
+ # A requested match raises the submission's own error, as it always has.
284
+ if policy_errors:
285
+ raise
286
+ raise (error.__cause__ or error) from None
287
+ actions = policies.actions(parts, answers)
288
+ if replay is not None:
289
+ stepped.append([int(value) for value in actions[0].tolist()])
290
+ _, _, finished, _ = batch.step(actions)
291
+ done = bool(finished[0])
292
+ frames.append(reader.frame(batch.get_state(0), 1 if done else 0))
293
+ if len(frames) % 50 == 0 and progress is not None:
294
+ progress(min(len(frames) / total, 0.99))
295
+ policies.finish()
296
+ except RuleError as broken:
297
+ found = policies.stats() if policies is not None else broken.stats
298
+ raise ForfeitError(broken, frame_score(frames[-1]), len(frames) - 1, found) from None
299
+ finally:
300
+ if policies is not None:
301
+ policies.close()
302
+ if stats is not None:
303
+ # A match that ended before its own end, by a rule or a limit of steps, says
304
+ # so, so that no report takes it for a full match.
305
+ stats.update(steps=len(frames) - 1, cut_short=not done, sides=policies.stats())
306
+ difficulty = batch.difficulty[0].tolist()
307
+ finally:
308
+ batch.close()
309
+ if not save:
310
+ return None
311
+ # A tournament's match names its sides as the tournament's entrants, such as a renamed team.
312
+ # A name or title that cuju makes travels as a label, and a person's as text.
313
+ entrants = side_names(tournament)
314
+ names = [entrants[side] or _teams.shown_name(team) for side, team in enumerate(sides)]
315
+ if not title:
316
+ title = game_title(names, tournament)
317
+ elif (
318
+ isinstance(title, str)
319
+ and tournament is not None
320
+ and not title.startswith(str(tournament.get('name')))
321
+ ):
322
+ title = f'{tournament.get("name")}, {tournament.get("label")}, {title}'
323
+ # The ability that played, the scenario's where none was given, goes with each team.
324
+ played = [round(float(value), 4) for value in difficulty]
325
+ teams = [
326
+ {
327
+ **{key: value for key, value in team.items() if key not in ('name_id', 'name_values')},
328
+ **_messages.named('name', entrants[side] or _teams.shown_name(team)),
329
+ 'ability': played[side],
330
+ }
331
+ for side, team in enumerate(_teams.match_teams(left, right))
332
+ ]
333
+ provenance = {
334
+ 'source': 'requested_match',
335
+ 'seed': seed,
336
+ 'scenario': scenario,
337
+ 'teams': [left, right],
338
+ 'controls': controls,
339
+ 'agent_counts': agents,
340
+ 'action_set': 'v2' if extended else 'default',
341
+ 'ability': played,
342
+ }
343
+ if length is not None:
344
+ provenance['length'] = int(length)
345
+ if draw is not None:
346
+ provenance['draw'] = draw
347
+ if tournament is not None:
348
+ provenance['tournament'] = tournament
349
+ head = _recording.header(title, scenario, lineup, teams, len(frames), timing, provenance)
350
+ path = _library.save(head, frames)
351
+ try:
352
+ _achievements.award_match(head, frames, [left, right], path.stem)
353
+ except Exception as error:
354
+ logging.getLogger('cuju').warning('cuju render could not record achievements, %s', error)
355
+ return path
356
+
357
+
358
+ class PolicyMessageError(_messages.Message, PolicyError):
359
+ """A `PolicyError` whose text is a message of the server, which the page translates."""
360
+
361
+ _wrapper = True
362
+
363
+
364
+ def frame_score(frame: bytes) -> list[int]:
365
+ """Returns the score of the left and the right side in a frame of a recording."""
366
+ return list(_SCORE.unpack_from(frame, 8))
367
+
368
+
369
+ class RuleError(PolicyError):
370
+ """Raised when a side breaks a rule under strict rules, with each side that broke one.
371
+
372
+ Attributes:
373
+ breaks: Each side that broke a rule at that moment, as `(side, kind, reason, detail)`,
374
+ the side 0 for the left and 1 for the right, the kind one of `tournament.RULES`, the
375
+ reason a label of a server message and the detail of an invalid action, `count`,
376
+ `type` or `range`, else `None`.
377
+ stats: The calls, timings and actions of each side, for a break before the policies of
378
+ the match have loaded.
379
+ """
380
+
381
+ def __init__(self, breaks: list[tuple[Any, ...]], stats: list[Any] | None = None) -> None:
382
+ self.breaks = [(*found, None)[:4] if len(found) == 3 else tuple(found) for found in breaks]
383
+ super().__init__('; '.join(found[2]['text'] for found in self.breaks))
384
+ self.stats = stats or [None, None]
385
+
386
+
387
+ class ForfeitError(Exception):
388
+ """Raised when a match under strict rules ends early, since a side broke a rule.
389
+
390
+ Attributes:
391
+ rule: The rule as a tournament's result keeps it, the side that broke it, `home` for the
392
+ left, `away` for the right or `both`, the kind, the reason as text with the id and
393
+ values of its message, the step and the score at that moment.
394
+ stats: The steps of the match and the calls, timings and actions of each side up to
395
+ that moment.
396
+ """
397
+
398
+ def __init__(self, broken: RuleError, score: list[int], step: int, stats: list[Any]) -> None:
399
+ sides = {found[0] for found in broken.breaks}
400
+ _, kind, reason, detail = broken.breaks[0]
401
+ if len(broken.breaks) > 1:
402
+ reason = _messages.label(
403
+ 'server.strict.both', home=broken.breaks[0][2], away=broken.breaks[1][2]
404
+ )
405
+ side = 'both' if len(sides) > 1 else ('home', 'away')[next(iter(sides))]
406
+ self.rule = {
407
+ 'side': side,
408
+ 'kind': kind,
409
+ 'reason': reason['text'],
410
+ 'reason_id': reason['id'],
411
+ 'reason_values': reason['values'],
412
+ 'detail': detail,
413
+ 'step': step,
414
+ 'at': score,
415
+ }
416
+ self.stats = {'steps': step, 'cut_short': True, 'sides': stats}
417
+ super().__init__(reason['text'])
418
+
419
+
420
+ class CallStats:
421
+ """The calls of one side's policy in a match, their compute times and the actions answered."""
422
+
423
+ def __init__(self) -> None:
424
+ self.calls = 0
425
+ self.total = 0.0
426
+ self.largest = 0.0
427
+ self.first: float | None = None
428
+ self.actions: collections.Counter[int] = collections.Counter()
429
+ self.load: float | None = None
430
+ self.reset: float | None = None
431
+ self.memory: float | None = None
432
+
433
+ def add(self, seconds: float, actions: list[int]) -> None:
434
+ """Counts one call that took `seconds` and answered `actions`."""
435
+ self.calls += 1
436
+ self.total += seconds
437
+ self.largest = max(self.largest, seconds)
438
+ if self.first is None:
439
+ self.first = seconds
440
+ self.actions.update(actions)
441
+
442
+ def mean(self, after_first: bool = False) -> float:
443
+ """Returns the mean seconds of a call, those after the first alone when asked."""
444
+ if after_first and self.calls > 1:
445
+ return (self.total - (self.first or 0.0)) / (self.calls - 1)
446
+ return self.total / self.calls if self.calls else 0.0
447
+
448
+ def summary(self) -> dict[str, Any]:
449
+ """Returns the calls, the first, mean and largest compute time in ms and the actions."""
450
+
451
+ def ms(value: float | None) -> float | None:
452
+ return None if value is None else round(value * 1000, 3)
453
+
454
+ found = {
455
+ 'calls': self.calls,
456
+ 'first_ms': ms(self.first),
457
+ 'mean_ms': ms(self.mean()) if self.calls else None,
458
+ 'max_ms': ms(self.largest) if self.calls else None,
459
+ 'mean_after_first_ms': ms(self.mean(True)) if self.calls else None,
460
+ 'actions': {str(action): count for action, count in sorted(self.actions.items())},
461
+ }
462
+ if self.load is not None:
463
+ found['load_seconds'] = round(self.load, 3)
464
+ if self.reset is not None:
465
+ found['reset_seconds'] = round(self.reset, 3)
466
+ if self.memory:
467
+ found['memory_mb'] = round(self.memory, 1)
468
+ return found
469
+
470
+
471
+ class RandomPolicy:
472
+ """The policy of the random entrant, an action of the default set at random for each call.
473
+
474
+ Its generator is seeded from the seed of the match and its side, so a match plays the same
475
+ actions again with the same seed.
476
+ """
477
+
478
+ def __init__(self, seed: int, side: int) -> None:
479
+ self.random = random.Random(hashlib.sha256(f'{seed}/random/{side}'.encode()).digest())
480
+
481
+ def act(self, obs: Any, count: int) -> list[int]:
482
+ """Returns one action of the 19 of the default set for each controlled player."""
483
+ del count
484
+ return [self.random.randrange(DEFAULT_ACTIONS) for _ in obs['players_raw']]
485
+
486
+
487
+ class Policies:
488
+ """The registered policies of a match, the agents each one plays and the actions they return.
489
+
490
+ A step comes in three parts, so the calls of the policies may run on a thread of their own
491
+ while another thread steps the batch. `observe` reads the batch, on the thread that steps it,
492
+ `decide` calls the policies with what `observe` read and touches nothing else, and `actions`
493
+ puts their answers into the actions of the step. Each policy counts Kaggle's `step` by its
494
+ own calls, as `KaggleAgent` does.
495
+
496
+ Attributes:
497
+ batch: The batch, with the raw representation.
498
+ players: One entry for each submission, its side, name, function and columns.
499
+ """
500
+
501
+ def __init__(
502
+ self,
503
+ batch: Any,
504
+ sides: list[dict[str, Any]],
505
+ scenario: str,
506
+ agents: list[int],
507
+ extended: bool,
508
+ firsts: list[int],
509
+ names: list[Any] | None = None,
510
+ fits: list[dict[str, Any] | None] | None = None,
511
+ seed: int = 0,
512
+ ) -> None:
513
+ """Loads the submission of each side that has one, and the random entrant's policy.
514
+
515
+ Args:
516
+ batch: The batch, which only `observe` reads.
517
+ sides: The registry entries of the left and the right team.
518
+ scenario: The scenario, which places the selected players of a fixed assignment.
519
+ agents: The agents of each side that its team plays.
520
+ extended: Whether the batch plays the v2 action set, so that a submission of the
521
+ default set has its answers checked against 19 actions.
522
+ firsts: The column of the first agent that each side's team plays.
523
+ names: The name of each side that its failure gives, text, a label or `None` for
524
+ the team's own, such as the entrants of a tournament, or `None` for both.
525
+ fits: How each side plays a challenge, as `challenge_fit` of the registry gives it,
526
+ with its selected players and its players out, or `None` for both sides of a
527
+ match, whose teams play every player they control.
528
+ seed: The seed of the match, from which the random entrant draws its actions.
529
+ """
530
+ self.batch = batch
531
+ self.players: list[dict[str, Any]] = []
532
+ for side, team in enumerate(sides):
533
+ if team['kind'] == 'builtin' or not agents[side]:
534
+ continue
535
+ columns = list(range(firsts[side], firsts[side] + agents[side]))
536
+ control = _teams.check_control(team.get('control'), team['format'])
537
+ fit = (fits or [None, None])[side]
538
+ selected = None
539
+ if fit is not None:
540
+ selected = fit['selected']
541
+ elif control['assignment'] == 'fixed':
542
+ selected = _teams.selected_players(control, _teams.scenario_players(scenario, side))
543
+ name = (names or [None, None])[side] or team['name']
544
+ self.players.append(
545
+ {
546
+ 'side': side,
547
+ 'name': name,
548
+ 'runner': self._runner(team, side, extended, control, seed, name),
549
+ 'columns': columns,
550
+ 'selected': selected,
551
+ 'slots': None if fit is None else fit['slots'],
552
+ 'held': {},
553
+ 'calls': 0,
554
+ 'stats': CallStats(),
555
+ }
556
+ )
557
+
558
+ def _runner(
559
+ self,
560
+ team: dict[str, Any],
561
+ side: int,
562
+ extended: bool,
563
+ control: dict[str, Any],
564
+ seed: int,
565
+ name: Any,
566
+ ) -> Any:
567
+ """Returns what answers the calls of a side's policy, loaded in this process."""
568
+ del name
569
+ if team['kind'] == 'random':
570
+ return RandomPolicy(seed, side)
571
+ function = _teams.load_agent(team['id'])
572
+ if extended and control['action_set'] == 'default':
573
+ function = _default_actions(function)
574
+ return AgentRunner(function)
575
+
576
+ def stats(self) -> list[dict[str, Any] | None]:
577
+ """Returns the calls, timings and actions of each side, `None` for a side without one."""
578
+ found: list[dict[str, Any] | None] = [None, None]
579
+ for player in self.players:
580
+ found[player['side']] = player.setdefault('stats', CallStats()).summary()
581
+ return found
582
+
583
+ def finish(self) -> None:
584
+ """Checks what only the whole match tells, as strict rules do for the mean of a call."""
585
+
586
+ def close(self) -> None:
587
+ """Ends what the policies hold, such as the processes of strict rules."""
588
+
589
+ def observe(self) -> list[Any]:
590
+ """Returns what each policy sees at the current step, on the thread that steps the batch.
591
+
592
+ Each entry holds the columns that the policy plays at the step, the columns of its selected
593
+ players who left the pitch, its observation, and the answers that count, in the order of
594
+ the columns, or `None` for all of them. An entry is `None` when GRF hands the players of a
595
+ fixed assignment over again, and the built-in AI plays them for that step. A team that
596
+ plays a challenge with players out still sees one observation for each player it
597
+ controls, and a player out sees the observation of its first player that plays.
598
+ """
599
+ raw = {name: tensor.cpu() for name, tensor in self.batch.raw().items()}
600
+ parts: list[Any] = []
601
+ for player in self.players:
602
+ columns, missing = player['columns'], []
603
+ if player['selected'] is not None:
604
+ found = _player_columns(raw, columns, player['selected'], player['held'])
605
+ if found is None:
606
+ parts.append(None)
607
+ continue
608
+ columns, missing = found
609
+ seen, pick = list(columns), None
610
+ slots = player.get('slots')
611
+ if slots is not None:
612
+ seen = [columns[0] if place is None else columns[place] for place in slots]
613
+ pick = [slots.index(place) for place in range(len(columns))]
614
+ obs = observation(self.batch, 0, seen, step=player['calls'], raw=raw)
615
+ player['calls'] += 1
616
+ parts.append((columns, missing, obs, pick))
617
+ return parts
618
+
619
+ def decide(self, parts: list[Any]) -> list[Any]:
620
+ """Calls each policy with its observation and returns its actions, checked.
621
+
622
+ Raises:
623
+ PolicyError: If a policy raises, or answers with anything but actions of the set.
624
+ """
625
+ count = self.batch.action_count
626
+ answers: list[Any] = []
627
+ for player, part in zip(self.players, parts, strict=True):
628
+ if part is None:
629
+ answers.append(None)
630
+ continue
631
+ obs = part[2]
632
+ started = time.perf_counter()
633
+ try:
634
+ # Kaggle's single form for one player, and its multi agent form for several.
635
+ answer = player['runner'].act(obs, count)
636
+ except Exception as error:
637
+ raise _policy_error(error, player['name']) from error
638
+ # A player that a caller added by hand, as a challenge does, counts its calls too.
639
+ player.setdefault('stats', CallStats()).add(time.perf_counter() - started, answer)
640
+ answers.append(answer)
641
+ return answers
642
+
643
+ def actions(self, parts: list[Any] | None, answers: list[Any] | None) -> torch.Tensor:
644
+ """Returns the actions of a step, `[1, agents]`, idle in the columns of no policy.
645
+
646
+ Without answers, the built-in AI plays every agent of the policies for the step, as the
647
+ v2 action set allows. It also plays the agents of a fixed assignment outside the selection,
648
+ and the selected players off the pitch.
649
+
650
+ Raises:
651
+ PolicyError: If the built-in AI should play an agent of a batch without the v2 set.
652
+ """
653
+ actions = torch.zeros((1, self.batch.agents), dtype=torch.int32)
654
+ for index, player in enumerate(self.players):
655
+ part = None if parts is None or answers is None else parts[index]
656
+ if part is None or player['selected'] is not None:
657
+ if part is None and self.batch.action_count <= _BUILTIN_AI:
658
+ raise PolicyMessageError('server.policy.action_set')
659
+ actions[:, player['columns']] = _BUILTIN_AI
660
+ if part is None:
661
+ continue
662
+ columns, missing, _, pick = part
663
+ answer = answers[index] if pick is None else [answers[index][slot] for slot in pick]
664
+ actions[:, columns] = torch.tensor(answer, dtype=torch.int32).reshape(1, -1)
665
+ # A selected player off the pitch, after a red card for one, is left to the built-in
666
+ # AI, and the submission keeps playing the others.
667
+ actions[:, missing] = _BUILTIN_AI
668
+ return actions
669
+
670
+
671
+ class _Job:
672
+ """One call on the thread of the policies, its answer or its error, and when it was asked."""
673
+
674
+ def __init__(self, function: Callable[[], Any]) -> None:
675
+ self.function = function
676
+ self.started = time.monotonic()
677
+ self.done = threading.Event()
678
+ self.value: Any = None
679
+ self.error: BaseException | None = None
680
+
681
+ def run(self) -> None:
682
+ try:
683
+ self.value = self.function()
684
+ except BaseException as error:
685
+ # A policy may raise anything, even an exit, and the caller hears of it all.
686
+ self.error = error
687
+ if debugging():
688
+ traceback.print_exc()
689
+ finally:
690
+ self.done.set()
691
+
692
+ def result(self) -> Any:
693
+ """Returns the answer, or raises the error as a `PolicyError`."""
694
+ if isinstance(self.error, PolicyError):
695
+ raise self.error
696
+ if self.error is not None:
697
+ raise _policy_error(self.error) from self.error
698
+ return self.value
699
+
700
+
701
+ class PolicyRunner:
702
+ """Runs the registered policies of a match on a thread of their own, one step at a time.
703
+
704
+ The thread that steps the batch asks for the actions of a step in two calls. `request` reads
705
+ the observations and starts the call of the policies, and `actions` waits for the answer
706
+ within a budget and returns the actions of the step. A policy that misses the budget leaves
707
+ its players to the built-in AI for that step, and its late answer is discarded. While a late
708
+ call still runs, the steps that follow go to the built-in AI without a call, since a policy
709
+ answers one observation at a time. The runner keeps no clock beyond the budget, so a game at
710
+ real time and a run faster than real time use it alike, the second without a budget.
711
+
712
+ Attributes:
713
+ policies: The policies, once `load` has returned.
714
+ hang: The seconds a call may run before the policy counts as stuck, or `None`.
715
+ late: The steps that the built-in AI played in place of the policies.
716
+ streak: The steps in a row, up to the last one, that the built-in AI played in their place.
717
+ """
718
+
719
+ def __init__(self, hang: float | None = None) -> None:
720
+ self.policies: Policies | None = None
721
+ self.hang = hang
722
+ self.late = 0
723
+ self.streak = 0
724
+ self._jobs: queue.Queue[_Job | None] = queue.Queue()
725
+ self._busy: _Job | None = None
726
+ self._turn: tuple[Any, _Job] | None = None
727
+ # A daemon thread, so that a policy that never returns cannot keep the process alive.
728
+ threading.Thread(target=self._work, daemon=True, name='cuju-policies').start()
729
+
730
+ def _work(self) -> None:
731
+ while (job := self._jobs.get()) is not None:
732
+ job.run()
733
+
734
+ def _submit(self, function: Callable[[], Any]) -> _Job:
735
+ job = _Job(function)
736
+ self._busy = job
737
+ self._jobs.put(job)
738
+ return job
739
+
740
+ def load(self, factory: Callable[[], Policies], timeout: float | None) -> None:
741
+ """Loads the policies on their thread, within `timeout` seconds or without a limit.
742
+
743
+ Raises:
744
+ PolicyError: If loading fails or takes longer than `timeout`.
745
+ """
746
+ job = self._submit(factory)
747
+ if not job.done.wait(timeout):
748
+ raise PolicyMessageError('server.policy.load', count=f'{timeout:g}')
749
+ self.policies = job.result()
750
+
751
+ def _check(self) -> None:
752
+ """Raises for a call that failed, even a late one, and for a call that runs too long."""
753
+ job = self._busy
754
+ if job is None:
755
+ return
756
+ if job.done.is_set():
757
+ if job.error is not None:
758
+ job.result()
759
+ return
760
+ if self.hang is not None and time.monotonic() - job.started > self.hang:
761
+ raise PolicyMessageError('server.policy.silent', count=f'{self.hang:g}')
762
+
763
+ def request(self) -> None:
764
+ """Reads the observations of the current step and starts the call of the policies.
765
+
766
+ Raises:
767
+ PolicyError: If a call failed, or has run longer than `hang`.
768
+ """
769
+ self._check()
770
+ if self._busy is not None and not self._busy.done.is_set():
771
+ # A late call still runs, so the built-in AI plays this step.
772
+ self._turn = None
773
+ return
774
+ policies = self.policies
775
+ parts = policies.observe()
776
+ self._turn = (parts, self._submit(lambda: policies.decide(parts)))
777
+
778
+ def wait(self, timeout: float | None) -> bool:
779
+ """Waits for the answer to the step requested last and returns whether it came in time.
780
+
781
+ Raises:
782
+ PolicyError: If the call failed.
783
+ """
784
+ if self._turn is None:
785
+ return False
786
+ job = self._turn[1]
787
+ if not job.done.wait(timeout):
788
+ return False
789
+ job.result()
790
+ return True
791
+
792
+ def actions(self, budget: float | None = None) -> torch.Tensor:
793
+ """Returns the actions of the step requested last, `[1, agents]`.
794
+
795
+ Args:
796
+ budget: The seconds from the request within which the answer counts, or `None` to wait
797
+ for the answer however long it takes.
798
+
799
+ Raises:
800
+ PolicyError: If the call failed.
801
+ """
802
+ turn, self._turn = self._turn, None
803
+ if turn is not None:
804
+ parts, job = turn
805
+ wait = None if budget is None else max(0.0, job.started + budget - time.monotonic())
806
+ if job.done.wait(wait):
807
+ answers = job.result()
808
+ self.streak = 0
809
+ return self.policies.actions(parts, answers)
810
+ self.late += 1
811
+ self.streak += 1
812
+ return self.policies.actions(None, None)
813
+
814
+ def close(self) -> None:
815
+ """Lets the thread of the policies end as soon as it is free."""
816
+ self._jobs.put(None)
817
+
818
+
819
+ def _player_columns(
820
+ raw: dict[str, torch.Tensor], columns: list[int], selected: list[int], held: dict[int, int]
821
+ ) -> tuple[list[int], list[int]] | None:
822
+ """Finds the agents holding the selected players in submission order, and those left over.
823
+
824
+ GRF clears the selection of a side at a set piece, and for that step an agent reports no player,
825
+ a step that returns `None`. A selected player that no agent reports for another reason, such as
826
+ a red card, keeps the agent it held last, or else an agent that holds no selected player, and
827
+ that agent is the second list. `held` keeps the agent each selected player held last.
828
+ """
829
+ active = [int(raw['active'][0, column]) for column in columns]
830
+ if -1 in active:
831
+ return None
832
+ by_player = {}
833
+ for column, index in zip(columns, active, strict=True):
834
+ if index in selected and index not in by_player:
835
+ by_player[index] = column
836
+ held.update(by_player)
837
+ free = [column for column in columns if column not in by_player.values()]
838
+ missing = []
839
+ for index in selected:
840
+ if index in by_player:
841
+ continue
842
+ column = held.get(index)
843
+ if column not in free:
844
+ column = free[0]
845
+ free.remove(column)
846
+ by_player[index] = column
847
+ missing.append(column)
848
+ return [by_player[index] for index in selected], missing
849
+
850
+
851
+ def _default_actions(function: Any) -> Any:
852
+ """Checks the submission set before translating negative indices to the batch set."""
853
+ two = accepts_configuration(function)
854
+
855
+ def run(obs: Any, configuration: Any) -> list[int]:
856
+ answer = function(obs, configuration) if two else function(obs)
857
+ count = len(obs['players_raw'])
858
+ values = [action_index(answer, 19)] if count == 1 else action_indices(answer, 19, count)
859
+ return [value % 19 for value in values]
860
+
861
+ return run
862
+
863
+
864
+ def main(arguments: list[str] | None = None) -> None:
865
+ """Plays the match that the command line names and reports the recording."""
866
+ global _reports
867
+ parser = argparse.ArgumentParser(prog='python -m cuju.render._playing.play')
868
+ parser.add_argument('--left', required=True)
869
+ parser.add_argument('--right', required=True)
870
+ parser.add_argument('--scenario', default='11_vs_11_kaggle')
871
+ parser.add_argument('--seed', type=int, default=None)
872
+ parser.add_argument('--device', default='cpu')
873
+ parser.add_argument('--left-ability', type=float, default=None)
874
+ parser.add_argument('--right-ability', type=float, default=None)
875
+ parser.add_argument('--length', type=int, default=None)
876
+ parser.add_argument('--tournament', type=json.loads, default=None)
877
+ parser.add_argument('--draw', type=json.loads, default=None)
878
+ parser.add_argument('--strict', type=json.loads, default=None)
879
+ parser.add_argument('--replay', action='store_true')
880
+ options = parser.parse_args(arguments)
881
+ # The reports keep a copy of standard output, and everything else written there, by Python or
882
+ # by native code, goes to standard error.
883
+ sys.stdout.flush()
884
+ _reports = os.fdopen(os.dup(1), 'w', encoding='utf-8')
885
+ os.dup2(2, 1)
886
+ sys.stdout = sys.stderr
887
+ stats: dict[str, Any] = {}
888
+ replay: dict[str, Any] | None = {} if options.replay else None
889
+ try:
890
+ path = play(
891
+ options.left,
892
+ options.right,
893
+ options.scenario,
894
+ options.seed,
895
+ None,
896
+ options.device,
897
+ (options.left_ability, options.right_ability),
898
+ options.length,
899
+ options.tournament,
900
+ lambda share: _report(progress=share),
901
+ # A tournament's failure names the team that failed, a requested match's stays.
902
+ policy_errors=options.tournament is not None,
903
+ draw=options.draw,
904
+ strict=options.strict,
905
+ stats=stats,
906
+ replay=replay,
907
+ )
908
+ except ForfeitError as forfeit:
909
+ # A side that broke a rule of strict rules ends the match, which reports the forfeit.
910
+ _report(progress=1.0, forfeit=forfeit.rule, stats=forfeit.stats, replay=replay)
911
+ return
912
+ except PolicyError as error:
913
+ _report(error=str(error), **_messages.id_fields(error))
914
+ sys.exit(1)
915
+ except Exception as error:
916
+ _report(error=_messages.described(error), **_messages.id_fields(error))
917
+ sys.exit(1)
918
+ _report(progress=1.0, match=path.stem, stats=stats, replay=replay)
919
+
920
+
921
+ if __name__ == '__main__':
922
+ # The match runs from the module as the package imports it, so its errors are the classes
923
+ # that strict rules raise, which a copy of this file run as the main module would not catch.
924
+ from cuju.render._playing import play as _module
925
+
926
+ _module.main()