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,587 @@
|
|
|
1
|
+
# Copyright 2026 The cuju Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""The command `cuju render tournament`, which makes, lists, shows, runs and changes tournaments."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from cuju.errors import ArgumentError
|
|
15
|
+
|
|
16
|
+
from .._text.messages import counted
|
|
17
|
+
from . import tournament as rules
|
|
18
|
+
from . import tournament_runner as _tournament_runner
|
|
19
|
+
|
|
20
|
+
# How `show` names the state of a match still to play, as the page names it.
|
|
21
|
+
_MATCH_STATES = {
|
|
22
|
+
'ready': 'in the queue',
|
|
23
|
+
'running': 'playing',
|
|
24
|
+
'playing': 'a person is playing',
|
|
25
|
+
'person': 'ready for a person',
|
|
26
|
+
'failed': 'failed',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _stage_format(stage: Any, defaults: dict[str, Any]) -> Any:
|
|
31
|
+
if not isinstance(stage, dict):
|
|
32
|
+
return None
|
|
33
|
+
return stage.get('format', defaults.get('format', rules.STAGE_DEFAULTS['format']))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _config(options: argparse.Namespace) -> dict[str, Any]:
|
|
37
|
+
"""Returns the configuration that the options of `new` describe."""
|
|
38
|
+
if options.config:
|
|
39
|
+
try:
|
|
40
|
+
config = json.loads(Path(options.config).read_text(encoding='utf-8'))
|
|
41
|
+
except ValueError as error:
|
|
42
|
+
raise ArgumentError(f'expected JSON in {options.config}, {error}') from error
|
|
43
|
+
if not isinstance(config, dict):
|
|
44
|
+
raise ArgumentError(f'expected a JSON object in {options.config}')
|
|
45
|
+
else:
|
|
46
|
+
config = {'preset': options.preset or 'quick_cup'}
|
|
47
|
+
if options.preset and options.config:
|
|
48
|
+
config['preset'] = options.preset
|
|
49
|
+
if 'stages' not in config and config.get('preset'):
|
|
50
|
+
base = rules.preset_config(config['preset'], _tournament_runner.presets())
|
|
51
|
+
config = {**base, **config}
|
|
52
|
+
entrants: list[dict[str, Any]] = []
|
|
53
|
+
for kind, value in options.entrants or []:
|
|
54
|
+
if kind == 'ability':
|
|
55
|
+
# Before the first entrant it is the standard, after an entrant that entrant's own.
|
|
56
|
+
word = 'standard' if entrants else 'scenario'
|
|
57
|
+
try:
|
|
58
|
+
ability = None if value == word else float(value)
|
|
59
|
+
except ValueError as error:
|
|
60
|
+
raise ArgumentError(
|
|
61
|
+
f'expected --ability with an ability from 0 to 1 or {word}, found {value!r}'
|
|
62
|
+
) from error
|
|
63
|
+
if entrants:
|
|
64
|
+
entrants[-1]['ability'] = ability
|
|
65
|
+
else:
|
|
66
|
+
config['ability'] = ability
|
|
67
|
+
elif kind == 'as':
|
|
68
|
+
# A team entered again takes a free name, such as Rex 2, unless --as gives one.
|
|
69
|
+
if not entrants:
|
|
70
|
+
raise ArgumentError(
|
|
71
|
+
'expected --as after the --team, --builtin or --person that it names'
|
|
72
|
+
)
|
|
73
|
+
entrants[-1]['name'] = value
|
|
74
|
+
elif kind == 'team':
|
|
75
|
+
entrants.append({'kind': 'team', 'team': value})
|
|
76
|
+
elif kind == 'person':
|
|
77
|
+
entrants.append({'kind': 'person', 'name': value})
|
|
78
|
+
elif kind == 'random':
|
|
79
|
+
entrants.append({'kind': 'random'})
|
|
80
|
+
else:
|
|
81
|
+
try:
|
|
82
|
+
ability = None if value in ('', 'any', 'scenario') else float(value)
|
|
83
|
+
except ValueError as error:
|
|
84
|
+
raise ArgumentError(
|
|
85
|
+
f'expected --builtin with an ability from 0 to 1 or any, found {value!r}'
|
|
86
|
+
) from error
|
|
87
|
+
entrants.append({'kind': 'builtin', 'ability': ability})
|
|
88
|
+
if entrants:
|
|
89
|
+
config['entrants'] = entrants
|
|
90
|
+
for key in ('name', 'seed', 'seeding', 'concurrency'):
|
|
91
|
+
if getattr(options, key) is not None:
|
|
92
|
+
config[key] = getattr(options, key)
|
|
93
|
+
if options.strict is not None:
|
|
94
|
+
config['strict'] = _limits(options.strict)
|
|
95
|
+
if options.preflight:
|
|
96
|
+
config['preflight'] = True
|
|
97
|
+
defaults = dict(config.get('defaults') or {})
|
|
98
|
+
for key in ('scenario', 'length'):
|
|
99
|
+
if getattr(options, key) is not None:
|
|
100
|
+
defaults[key] = getattr(options, key)
|
|
101
|
+
config['defaults'] = defaults
|
|
102
|
+
if options.rounds is not None:
|
|
103
|
+
stages = config.get('stages') if isinstance(config.get('stages'), list) else []
|
|
104
|
+
rounded = [s for s in stages if _stage_format(s, defaults) in ('swiss', 'ladder')]
|
|
105
|
+
if not rounded:
|
|
106
|
+
raise ArgumentError('expected a Swiss or ladder stage for --rounds, found none')
|
|
107
|
+
for stage in rounded:
|
|
108
|
+
stage['rounds'] = options.rounds
|
|
109
|
+
return config
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _limits(text: str) -> Any:
|
|
113
|
+
"""Returns the limits of strict rules that `--strict` gives, true for the defaults."""
|
|
114
|
+
try:
|
|
115
|
+
found = json.loads(text)
|
|
116
|
+
except ValueError as error:
|
|
117
|
+
raise ArgumentError(f'expected --strict with a JSON object of limits, {error}') from error
|
|
118
|
+
if found == {}:
|
|
119
|
+
return True
|
|
120
|
+
if not isinstance(found, dict):
|
|
121
|
+
raise ArgumentError(f'expected --strict with a JSON object of limits, found {text!r}')
|
|
122
|
+
return found
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_lines(report: dict[str, Any]) -> list[str]:
|
|
126
|
+
"""Returns the lines that `preflight` prints for the check of one team."""
|
|
127
|
+
name = rules.text_of(report.get('name')) or report.get('team')
|
|
128
|
+
verdict = 'pass' if report.get('ok') else 'fail'
|
|
129
|
+
lines = [f'{verdict} {name} {report.get("team")} {report.get("scenario")}']
|
|
130
|
+
rule = report.get('rule')
|
|
131
|
+
if rule:
|
|
132
|
+
lines.append(f' {rule.get("kind")}, {rule.get("reason")}')
|
|
133
|
+
for side, found in (report.get('sides') or {}).items():
|
|
134
|
+
timings = found.get('timings') or {}
|
|
135
|
+
lines.append(f' {side}: {_timing_text(timings)}')
|
|
136
|
+
if not report.get('sides'):
|
|
137
|
+
lines.append(f' {_timing_text(report.get("timings") or {})}')
|
|
138
|
+
if report.get('folder_mb') is not None:
|
|
139
|
+
lines.append(f' folder {report["folder_mb"]:.1f} MB')
|
|
140
|
+
for warning in report.get('warnings') or []:
|
|
141
|
+
lines.append(f' warning, {rules.text_of(warning)}')
|
|
142
|
+
return lines
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _timing_text(timings: dict[str, Any]) -> str:
|
|
146
|
+
def part(key: str, words: str, unit: str) -> str | None:
|
|
147
|
+
value = timings.get(key)
|
|
148
|
+
return None if value is None else f'{words} {value:g} {unit}'
|
|
149
|
+
|
|
150
|
+
parts = [
|
|
151
|
+
part('load_seconds', 'loaded in', 's'),
|
|
152
|
+
part('reset_seconds', 'reset in', 's'),
|
|
153
|
+
part('first_ms', 'first call', 'ms'),
|
|
154
|
+
part('mean_after_first_ms', 'mean after the first', 'ms'),
|
|
155
|
+
part('max_ms', 'largest', 'ms'),
|
|
156
|
+
]
|
|
157
|
+
calls = timings.get('calls') or 0
|
|
158
|
+
parts.append(counted(calls, 'call'))
|
|
159
|
+
return ', '.join(item for item in parts if item)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _check_rounds(config: dict[str, Any], rounds: int) -> None:
|
|
163
|
+
"""Refuses `--rounds` beyond what a Swiss stage allows, which a preset would bring down."""
|
|
164
|
+
checked = _tournament_runner.check_config(config)
|
|
165
|
+
for index, stage in enumerate(checked['stages']):
|
|
166
|
+
if stage['format'] == 'swiss' and stage['rounds'] != rounds:
|
|
167
|
+
raise ArgumentError(
|
|
168
|
+
f'expected --rounds from 1 to {stage["rounds"]} for the Swiss stage {index + 1}, '
|
|
169
|
+
f'which pairs {stage["entrants"]} entrants without rematches, found {rounds}'
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _stage_title(stage: dict[str, Any]) -> str:
|
|
174
|
+
"""Returns a stage's name with its format, the format left out when the name is the format."""
|
|
175
|
+
title = rules._FORMAT_TITLES.get(stage['format'], stage['format'])
|
|
176
|
+
if stage['name'].casefold() == title.casefold():
|
|
177
|
+
return stage['name']
|
|
178
|
+
return f'{stage["name"]}, {title[0].lower()}{title[1:]}'
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _names(config: dict[str, Any]) -> list[str]:
|
|
182
|
+
"""Returns the name of each entrant, with its ability where it differs from the standard."""
|
|
183
|
+
names = []
|
|
184
|
+
for entrant in config['entrants']:
|
|
185
|
+
shown = rules.shown_ability(entrant, config.get('ability'))
|
|
186
|
+
names.append(entrant['name'] if shown is None else f'{entrant["name"]} at {shown:.2f}')
|
|
187
|
+
return names
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _ability_line(config: dict[str, Any]) -> str:
|
|
191
|
+
"""Returns the line that names the standard ability of a tournament."""
|
|
192
|
+
standard = config.get('ability')
|
|
193
|
+
return 'standard ability ' + ("the scenario's" if standard is None else f'{standard:.2f}')
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _played_line(entry: dict[str, Any]) -> str:
|
|
197
|
+
"""Returns how many matches a tournament played of those it expects, and those scheduled."""
|
|
198
|
+
played, scheduled = entry['played'], entry['scheduled']
|
|
199
|
+
expected = max(entry.get('expected') or 0, scheduled)
|
|
200
|
+
if expected == scheduled:
|
|
201
|
+
return f'{played} of {scheduled} scheduled matches played'
|
|
202
|
+
return f'{played} of {expected} matches played, {scheduled} scheduled so far'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def describe(entry: dict[str, Any]) -> list[str]:
|
|
206
|
+
"""Returns the lines that `show` prints, the stages with their tables, results and ranking."""
|
|
207
|
+
names = _names(entry['config'])
|
|
208
|
+
awarded = {
|
|
209
|
+
match['key'] for match in entry['matches'] if (match.get('result') or {}).get('forfeit')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
def name(side: Any) -> str:
|
|
213
|
+
if isinstance(side, dict):
|
|
214
|
+
return side.get('from', '')
|
|
215
|
+
return 'a bye' if side is None else names[side]
|
|
216
|
+
|
|
217
|
+
lines = [
|
|
218
|
+
f'{entry["name"]} {entry["id"]} {entry["state"]} seed {entry["seed"]}',
|
|
219
|
+
f'{_played_line(entry)}, {_ability_line(entry["config"])}',
|
|
220
|
+
]
|
|
221
|
+
if entry.get('error'):
|
|
222
|
+
lines.append(entry['error'])
|
|
223
|
+
# A cancelled tournament plays nothing more, so its unfinished stages read as cancelled.
|
|
224
|
+
cancelled = entry['state'] == 'cancelled'
|
|
225
|
+
for stage in entry['stages_view']:
|
|
226
|
+
lines.append('')
|
|
227
|
+
state = _STAGE_STATES.get(stage['state'], stage['state'])
|
|
228
|
+
if cancelled and stage['state'] != 'done':
|
|
229
|
+
state = 'cancelled'
|
|
230
|
+
lines.append(f'{_stage_title(stage)}, {state}')
|
|
231
|
+
swiss = stage['format'] == 'swiss'
|
|
232
|
+
for table in stage['tables']:
|
|
233
|
+
lines.append(f' {table["name"]}')
|
|
234
|
+
head = f' {"":>3} {"":<28} Played Won Drawn Lost Goals Points'
|
|
235
|
+
lines.append(head + (' Buchholz Byes' if swiss else ''))
|
|
236
|
+
for row in table['rows']:
|
|
237
|
+
goals = f'{row["for"]}:{row["against"]}'
|
|
238
|
+
line = (
|
|
239
|
+
f' {row["position"]:>3} {names[row["entrant"]]:<28} {row["played"]:>6} '
|
|
240
|
+
f'{row["won"]:>4} {row["drawn"]:>5} {row["lost"]:>4} {goals:>7} '
|
|
241
|
+
f'{row["points"]:>6}'
|
|
242
|
+
)
|
|
243
|
+
if swiss:
|
|
244
|
+
line += f' {row["buchholz"]:>8} {row["byes"]:>4}'
|
|
245
|
+
lines.append(line)
|
|
246
|
+
for round_ in stage['rounds']:
|
|
247
|
+
extra = ''
|
|
248
|
+
if round_.get('bye') is not None:
|
|
249
|
+
extra += f', bye for {names[round_["bye"]]}'
|
|
250
|
+
if round_.get('king') is not None:
|
|
251
|
+
extra += f', king {names[round_["king"]]}'
|
|
252
|
+
lines.append(f' {round_["name"]}{extra}')
|
|
253
|
+
for fixture in round_['fixtures']:
|
|
254
|
+
line = _fixture_line(fixture, name, awarded, cancelled)
|
|
255
|
+
if line:
|
|
256
|
+
lines.append(line)
|
|
257
|
+
waiting = [match for match in entry['matches'] if match.get('state') != 'done']
|
|
258
|
+
if waiting:
|
|
259
|
+
lines += ['', 'Matches not played' if cancelled else 'Matches to play']
|
|
260
|
+
for match in waiting:
|
|
261
|
+
state = _MATCH_STATES.get(match.get('state'), match.get('state'))
|
|
262
|
+
if cancelled:
|
|
263
|
+
state = 'not played, the tournament was cancelled'
|
|
264
|
+
if match.get('error'):
|
|
265
|
+
state = f'{state}, {match["error"]}'
|
|
266
|
+
lines.append(
|
|
267
|
+
f' {match["key"]} {match["label"]}, {names[match["home"]]} against '
|
|
268
|
+
f'{names[match["away"]]}, {state}'
|
|
269
|
+
)
|
|
270
|
+
if entry.get('ranking'):
|
|
271
|
+
lines += ['', 'Final ranking']
|
|
272
|
+
lines += [f' {place + 1:>3}. {names[e]}' for place, e in enumerate(entry['ranking'])]
|
|
273
|
+
if entry['champion'] is not None:
|
|
274
|
+
lines += ['', f'Champion: {entry["champion"]}']
|
|
275
|
+
return lines
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _fixture_line(
|
|
279
|
+
fixture: dict[str, Any], name: Callable[[Any], str], awarded: set[str], cancelled: bool
|
|
280
|
+
) -> str | None:
|
|
281
|
+
"""Returns the line of a tie in `show`, its score and then how it was settled, in the order
|
|
282
|
+
it happened, or `None` for a tie of two byes, which plays nothing."""
|
|
283
|
+
home, away = fixture['home'], fixture['away']
|
|
284
|
+
if fixture.get('decided_by') == 'bye' or home is None or away is None:
|
|
285
|
+
if home is None and away is None:
|
|
286
|
+
return None
|
|
287
|
+
return f' {name(away if home is None else home)}, a bye'
|
|
288
|
+
score = fixture.get('score')
|
|
289
|
+
done = fixture.get('state') == 'done'
|
|
290
|
+
if score is not None:
|
|
291
|
+
text = f' {score[0]} to {score[1]}' + ('' if done else ' so far')
|
|
292
|
+
elif done:
|
|
293
|
+
text = ''
|
|
294
|
+
else:
|
|
295
|
+
text = ' not played' + (', the tournament was cancelled' if cancelled else '')
|
|
296
|
+
earlier = False
|
|
297
|
+
for key, words in (('replay', 'replay'), ('decider', 'deciding matches')):
|
|
298
|
+
if fixture.get(key):
|
|
299
|
+
text += f', {words} {fixture[key][0]} to {fixture[key][1]}'
|
|
300
|
+
earlier = True
|
|
301
|
+
how = fixture.get('decided_by')
|
|
302
|
+
winner = fixture.get('winner')
|
|
303
|
+
if how in _DECIDED and isinstance(winner, int):
|
|
304
|
+
text += ', ' + _DECIDED[how].format(
|
|
305
|
+
winner=name(winner), then='then ' if earlier and how == 'lots' else ''
|
|
306
|
+
)
|
|
307
|
+
if awarded & set(fixture.get('matches', [])):
|
|
308
|
+
text += ', awarded'
|
|
309
|
+
return f' {name(home)} against {name(away)}{text}'
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def preview_lines(preview: dict[str, Any]) -> list[str]:
|
|
313
|
+
"""Returns the lines that `check` prints, each stage with its entrants and first draw."""
|
|
314
|
+
config = preview['config']
|
|
315
|
+
names = _names(config)
|
|
316
|
+
|
|
317
|
+
def name(side: Any) -> str:
|
|
318
|
+
if isinstance(side, dict):
|
|
319
|
+
return side.get('from', '')
|
|
320
|
+
return 'a bye' if side is None else names[side]
|
|
321
|
+
|
|
322
|
+
lines = [
|
|
323
|
+
f'{config["name"]}, seed {config["seed"]}, {counted(len(names), "entrant")}, '
|
|
324
|
+
f'{counted(preview["matches"], "match", "matches")} known before any result, '
|
|
325
|
+
f'{_ability_line(config)}'
|
|
326
|
+
]
|
|
327
|
+
last = len(config['stages']) - 1
|
|
328
|
+
pairs = zip(config['stages'], preview['stages'], strict=True)
|
|
329
|
+
for index, (settings, stage) in enumerate(pairs):
|
|
330
|
+
parts = [counted(settings['entrants'], 'entrant')]
|
|
331
|
+
if index < last:
|
|
332
|
+
advance = settings['advance']
|
|
333
|
+
parts.append(f'{advance} advances' if advance == 1 else f'{advance} advance')
|
|
334
|
+
parts.append(settings['scenario'])
|
|
335
|
+
if settings['length']:
|
|
336
|
+
parts.append(f'{counted(settings["length"], "step")} a match')
|
|
337
|
+
if settings.get('ability') is not None:
|
|
338
|
+
parts.append(f'ability {settings["ability"]:.2f}')
|
|
339
|
+
lines.append(f'{index + 1}. {_stage_title(settings)}, {", ".join(parts)}')
|
|
340
|
+
if settings['format'] == 'groups':
|
|
341
|
+
for table in stage['tables']:
|
|
342
|
+
members = ', '.join(names[row['entrant']] for row in table['rows'])
|
|
343
|
+
lines.append(f' {table["name"]}, {members}')
|
|
344
|
+
elif stage['rounds']:
|
|
345
|
+
first = stage['rounds'][0]
|
|
346
|
+
lines.append(f' {first["name"]}')
|
|
347
|
+
for fixture in first['fixtures']:
|
|
348
|
+
lines.append(f' {name(fixture["home"])} against {name(fixture["away"])}')
|
|
349
|
+
return lines
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _print_json(value: Any) -> None:
|
|
353
|
+
print(json.dumps(value, indent=2))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _plays_it(identifier: str) -> str:
|
|
357
|
+
return f'a running cuju render serve plays it, or cuju render tournament run {identifier}'
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _plays_on(entry: dict[str, Any]) -> str:
|
|
361
|
+
"""Returns who plays a tournament on after a change, the process that holds it when one does."""
|
|
362
|
+
if entry.get('owner'):
|
|
363
|
+
return f'and process {entry["owner"]}, which holds it, plays it on'
|
|
364
|
+
return _plays_it(entry['id'])
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def run_command(parser: argparse.ArgumentParser, options: argparse.Namespace) -> int:
|
|
368
|
+
"""Runs one action of `cuju render tournament` and returns the exit status."""
|
|
369
|
+
try:
|
|
370
|
+
return _run_command(parser, options)
|
|
371
|
+
except rules.TournamentError as error:
|
|
372
|
+
raise ArgumentError(str(error)) from error
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _run(options: argparse.Namespace) -> int:
|
|
376
|
+
"""Runs a tournament in this process and prints how it stopped, or the whole of it as JSON."""
|
|
377
|
+
tournaments = _tournament_runner.Tournaments(only=options.target, start=False)
|
|
378
|
+
try:
|
|
379
|
+
state = tournaments.run(options.target, report=_quiet if options.json else print)
|
|
380
|
+
entry = tournaments.get(options.target)
|
|
381
|
+
finally:
|
|
382
|
+
tournaments.close()
|
|
383
|
+
if options.json:
|
|
384
|
+
_print_json({'outcome': state, 'tournament': entry})
|
|
385
|
+
else:
|
|
386
|
+
print(_RUN_MESSAGES[state])
|
|
387
|
+
return 0 if state in ('done', 'people') else 1
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _preflight(parser: argparse.ArgumentParser, options: argparse.Namespace) -> int:
|
|
391
|
+
"""Checks one team as a tournament's pre-flight does, exit status 0 for a pass and 1 else."""
|
|
392
|
+
team = options.target or next(
|
|
393
|
+
(value for kind, value in options.entrants or [] if kind == 'team'), None
|
|
394
|
+
)
|
|
395
|
+
if not team:
|
|
396
|
+
parser.error('tournament preflight needs a team, as --team or the target')
|
|
397
|
+
limits = rules.check_strict(_limits(options.strict) if options.strict else True)
|
|
398
|
+
report = _tournament_runner.preflight(team, options.scenario, limits)
|
|
399
|
+
if options.json:
|
|
400
|
+
_print_json(report)
|
|
401
|
+
else:
|
|
402
|
+
print('\n'.join(check_lines(report)))
|
|
403
|
+
return 0 if report.get('ok') else 1
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _quiet(line: str) -> None:
|
|
407
|
+
del line
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _run_command(parser: argparse.ArgumentParser, options: argparse.Namespace) -> int:
|
|
411
|
+
action = options.action
|
|
412
|
+
if action == 'award':
|
|
413
|
+
action = 'forfeit'
|
|
414
|
+
if action in _NEEDS_TARGET and not options.target:
|
|
415
|
+
parser.error(f'tournament {options.action} needs the identifier of a tournament')
|
|
416
|
+
if action == 'forfeit' and (not options.match or not options.winner):
|
|
417
|
+
parser.error(
|
|
418
|
+
f'tournament {options.action} needs --match, the key of a match that tournament '
|
|
419
|
+
'show lists, and --winner, home or away'
|
|
420
|
+
)
|
|
421
|
+
if action == 'presets':
|
|
422
|
+
presets = _tournament_runner.presets()
|
|
423
|
+
if options.json:
|
|
424
|
+
_print_json(presets)
|
|
425
|
+
else:
|
|
426
|
+
for key, preset in presets.items():
|
|
427
|
+
print(f'{key} {preset["title"]}')
|
|
428
|
+
return 0
|
|
429
|
+
if action == 'ratings':
|
|
430
|
+
table = _tournament_runner.ratings()
|
|
431
|
+
if options.json:
|
|
432
|
+
_print_json(table)
|
|
433
|
+
else:
|
|
434
|
+
ordered = sorted(table.items(), key=lambda item: -item[1]['rating'])
|
|
435
|
+
for key, value in ordered:
|
|
436
|
+
matches = value.get('matches', 0)
|
|
437
|
+
plural = 'match' if matches == 1 else 'matches'
|
|
438
|
+
print(f'{value["rating"]:>8.1f} {matches:>4} {plural:<7} {value["name"]} {key}')
|
|
439
|
+
if not ordered:
|
|
440
|
+
print('no ratings yet, every match a tournament plays moves the ratings')
|
|
441
|
+
return 0
|
|
442
|
+
if action == 'check':
|
|
443
|
+
config = _config(options)
|
|
444
|
+
if options.rounds is not None:
|
|
445
|
+
_check_rounds(config, options.rounds)
|
|
446
|
+
preview = _tournament_runner.preview(config)
|
|
447
|
+
if options.json:
|
|
448
|
+
_print_json(preview)
|
|
449
|
+
else:
|
|
450
|
+
print('\n'.join(preview_lines(preview)))
|
|
451
|
+
return 0
|
|
452
|
+
if action == 'run':
|
|
453
|
+
return _run(options)
|
|
454
|
+
if action == 'preflight':
|
|
455
|
+
return _preflight(parser, options)
|
|
456
|
+
tournaments = _tournament_runner.Tournaments(start=False)
|
|
457
|
+
try:
|
|
458
|
+
if action == 'new':
|
|
459
|
+
config = _config(options)
|
|
460
|
+
if options.rounds is not None:
|
|
461
|
+
_check_rounds(config, options.rounds)
|
|
462
|
+
entry = tournaments.create(config)
|
|
463
|
+
if options.run:
|
|
464
|
+
if not options.json:
|
|
465
|
+
print(f'created {entry["id"]} {entry["name"]}')
|
|
466
|
+
tournaments.close()
|
|
467
|
+
options.target = entry['id']
|
|
468
|
+
return _run(options)
|
|
469
|
+
if options.json:
|
|
470
|
+
_print_json(entry)
|
|
471
|
+
else:
|
|
472
|
+
print(f'created {entry["id"]} {entry["name"]}')
|
|
473
|
+
print(_plays_it(entry['id']))
|
|
474
|
+
elif action == 'list':
|
|
475
|
+
entries = tournaments.entries()
|
|
476
|
+
if options.json:
|
|
477
|
+
_print_json(entries)
|
|
478
|
+
elif not entries:
|
|
479
|
+
print('no tournaments yet, cuju render tournament new makes one')
|
|
480
|
+
else:
|
|
481
|
+
for entry in entries:
|
|
482
|
+
champion = f' won by {entry["champion"]}' if entry['champion'] else ''
|
|
483
|
+
print(
|
|
484
|
+
f'{entry["id"]} {entry["state"]:<9} {entry["played"]:>4} played '
|
|
485
|
+
f'{entry["name"]}{champion}'
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
if tournaments.get(options.target) is None:
|
|
489
|
+
parser.error(f'no tournament {options.target!r}')
|
|
490
|
+
if action == 'show':
|
|
491
|
+
entry = tournaments.get(options.target)
|
|
492
|
+
if options.json:
|
|
493
|
+
_print_json(entry)
|
|
494
|
+
else:
|
|
495
|
+
print('\n'.join(describe(entry)))
|
|
496
|
+
elif action == 'forfeit':
|
|
497
|
+
entry = tournaments.forfeit(options.target, options.match, options.winner)
|
|
498
|
+
if options.json:
|
|
499
|
+
_print_json(entry)
|
|
500
|
+
return 0
|
|
501
|
+
match = next(m for m in entry['matches'] if m['key'] == options.match)
|
|
502
|
+
side = match['home'] if options.winner == 'home' else match['away']
|
|
503
|
+
print(f'awarded {match["label"]} to {entry["config"]["entrants"][side]["name"]}')
|
|
504
|
+
if entry['state'] == 'running':
|
|
505
|
+
print(f'the tournament runs, {_plays_on(entry)}')
|
|
506
|
+
elif entry['error']:
|
|
507
|
+
print(f'the tournament stays {entry["state"]}, {entry["error"]}')
|
|
508
|
+
elif action == 'resume':
|
|
509
|
+
entry = tournaments.resume(options.target)
|
|
510
|
+
if options.json:
|
|
511
|
+
_print_json(entry)
|
|
512
|
+
return 0
|
|
513
|
+
print(f'resumed {entry["id"]}, {_plays_on(entry)}')
|
|
514
|
+
elif action == 'export':
|
|
515
|
+
files = tournaments.files(options.target)
|
|
516
|
+
target = Path(options.to or '.')
|
|
517
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
518
|
+
for name, text in files.items():
|
|
519
|
+
(target / name).write_text(text, encoding='utf-8')
|
|
520
|
+
if options.json:
|
|
521
|
+
_print_json({'id': options.target, 'folder': str(target), 'files': list(files)})
|
|
522
|
+
else:
|
|
523
|
+
print(f'wrote {len(files)} files of results to {target}')
|
|
524
|
+
elif action == 'legs':
|
|
525
|
+
if options.legs is None:
|
|
526
|
+
parser.error('tournament legs needs --legs, the legs to add to the last stage')
|
|
527
|
+
entry = tournaments.add_legs(options.target, options.legs)
|
|
528
|
+
if options.json:
|
|
529
|
+
_print_json(entry)
|
|
530
|
+
else:
|
|
531
|
+
print(f'added {counted(options.legs, "leg")}, {_played_line(entry)}')
|
|
532
|
+
if entry['state'] == 'running':
|
|
533
|
+
print(f'the tournament runs, {_plays_on(entry)}')
|
|
534
|
+
elif action == 'replay':
|
|
535
|
+
if not options.match:
|
|
536
|
+
parser.error('tournament replay needs --match, the key of a match')
|
|
537
|
+
found = _tournament_runner.replay_match(options.target, options.match)
|
|
538
|
+
if options.json:
|
|
539
|
+
_print_json(found)
|
|
540
|
+
else:
|
|
541
|
+
again, kept = found['replay'], found['kept']
|
|
542
|
+
print(
|
|
543
|
+
f'{options.match} replays in {again["steps"]} steps to '
|
|
544
|
+
f'{again["score"][0]} to {again["score"][1]}, '
|
|
545
|
+
+ ('as it played' if found['same'] else f'though it played {kept}')
|
|
546
|
+
)
|
|
547
|
+
return 0 if found['same'] else 1
|
|
548
|
+
elif action == 'delete':
|
|
549
|
+
tournaments.remove(options.target)
|
|
550
|
+
if options.json:
|
|
551
|
+
_print_json({'id': options.target, 'deleted': True})
|
|
552
|
+
else:
|
|
553
|
+
print(f'deleted {options.target}, its recordings stay in the library')
|
|
554
|
+
else:
|
|
555
|
+
tournaments.cancel(options.target)
|
|
556
|
+
if options.json:
|
|
557
|
+
_print_json(tournaments.get(options.target))
|
|
558
|
+
else:
|
|
559
|
+
print(f'cancelled {options.target}')
|
|
560
|
+
finally:
|
|
561
|
+
tournaments.close()
|
|
562
|
+
return 0
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
_NEEDS_TARGET = ('show', 'run', 'cancel', 'forfeit', 'resume', 'delete', 'export', 'legs', 'replay')
|
|
566
|
+
_RUN_MESSAGES = {
|
|
567
|
+
'done': 'the tournament is complete',
|
|
568
|
+
'cancelled': 'the tournament was cancelled',
|
|
569
|
+
'stopped': 'the tournament stopped, award the failed matches with cuju render '
|
|
570
|
+
'tournament forfeit, or run it again to retry them',
|
|
571
|
+
'people': 'the tournament waits for people, who play their matches on the Play page',
|
|
572
|
+
}
|
|
573
|
+
# How `show` names the state of a stage and how a tie was decided.
|
|
574
|
+
_STAGE_STATES = {
|
|
575
|
+
'waiting': 'waiting for the stage before it',
|
|
576
|
+
'ready': 'ready',
|
|
577
|
+
'playing': 'playing',
|
|
578
|
+
'done': 'finished',
|
|
579
|
+
}
|
|
580
|
+
# Each follows the score, the replay and the deciding matches, so it names only who went on.
|
|
581
|
+
_DECIDED = {
|
|
582
|
+
'away goals': '{winner} wins on away goals',
|
|
583
|
+
'replay': '{winner} wins',
|
|
584
|
+
'decider': '{winner} wins',
|
|
585
|
+
'lots': '{then}{winner} wins by lots',
|
|
586
|
+
'holder': '{winner} keeps the place on a draw',
|
|
587
|
+
}
|