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