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