cuju-render 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,113 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The names and titles that the server makes, such as the built-in AI, the sides Left and Right,
5
+ // You or the title of a recording, in the language of the page. The server sends each such name
6
+ // as a label, its English text in `name` or `title` with the message beside it in `name_id` and
7
+ // `name_values`, or `title_id` and `title_values`, and a name that a person gave as text alone,
8
+ // which shows as given. A recording of before holds the English text alone and shows it. A
9
+ // field of a malformed file that is no text shows as the empty text, or as the digits of a number.
10
+
11
+ import { labelText, plainText, serverText, setValueKind, tOr } from './i18n.js';
12
+ import { titleText } from './scenarios.js';
13
+
14
+ // A challenge that a message names, as in the title of an attempt, shows by its own title.
15
+ setValueKind('challenge', (name, label) => tOr(`challenge.${name}.title`, (label && label.text) || name));
16
+
17
+ // The messages that the names and titles of recordings carry, the names that cuju makes and the
18
+ // labels of tournaments. A name or a title that names any other message shows its English text.
19
+ const NAME_MESSAGE = /^server\.(names|tournament\.label)\./;
20
+ const isNameMessage = (id) => typeof id === 'string' && NAME_MESSAGE.test(id);
21
+
22
+ // The shipped samples, whose files keep the English title they ship with, by recording id.
23
+ const SAMPLES = {
24
+ 'sample-5v5': ['Sample match, 5 vs 5', 'server.names.sample_5v5'],
25
+ 'sample-11v11': ['Sample match, 11 vs 11', 'server.names.sample_11v11'],
26
+ };
27
+
28
+ // The name of a team, of a recording's header, of the registry or of a game, in the page's
29
+ // language, or the empty text for none.
30
+ export function teamName(team) {
31
+ if (!team || typeof team !== 'object') {
32
+ return typeof team === 'string' ? team : '';
33
+ }
34
+ return typeof team.name === 'string' ? serverText(team, 'name', isNameMessage) : '';
35
+ }
36
+
37
+ // A team's name as a requested match keeps it, the label of a name that cuju made in the page's
38
+ // language, or the text that a person gave, and the empty text for anything else.
39
+ export function keptName(value) {
40
+ return value && typeof value === 'object' ? labelText(value, isNameMessage) : plainText(value ?? '');
41
+ }
42
+
43
+ // The names of the two teams of a summary of the library or of a game's entry, which hold the
44
+ // English names in `teams` and the label of each made name in `team_labels`.
45
+ export function summaryTeams(summary) {
46
+ const names = summary && Array.isArray(summary.teams) ? summary.teams : [];
47
+ const labels = summary && Array.isArray(summary.team_labels) ? summary.team_labels : [];
48
+ return names.map((name, i) => (labels[i] && typeof labels[i] === 'object' ? labelText(labels[i], isNameMessage) : plainText(name)));
49
+ }
50
+
51
+ // The texts of a list of a summary, such as its English team names or its tags, and the empty list
52
+ // for anything that is no list.
53
+ export function textList(value) {
54
+ return Array.isArray(value) ? value.map(plainText) : [];
55
+ }
56
+
57
+ // The summaries of the library as the lists of the page read them. A field of the wrong kind, as
58
+ // a malformed header leaves it, becomes text, a list of texts or nothing, so every list shows each
59
+ // recording, its id where its title is no text. Anything but a list of summaries is no recording.
60
+ const TEXT_FIELDS = ['title', 'scenario', 'notes', 'folder'];
61
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
62
+ export function summaryList(list) {
63
+ return Array.isArray(list) ? list.filter((m) => isObject(m) && typeof m.id === 'string').map(cleanSummary) : [];
64
+ }
65
+
66
+ function cleanSummary(summary) {
67
+ const m = { ...summary };
68
+ for (const key of TEXT_FIELDS) {
69
+ if (key in m && typeof m[key] !== 'string') {
70
+ m[key] = plainText(m[key]);
71
+ }
72
+ }
73
+ for (const key of ['teams', 'tags']) {
74
+ if (key in m) {
75
+ m[key] = textList(m[key]);
76
+ }
77
+ }
78
+ if ('team_labels' in m) {
79
+ m.team_labels = Array.isArray(m.team_labels) ? m.team_labels.map((label) => (isObject(label) ? label : null)) : null;
80
+ }
81
+ if ('score' in m && !(Array.isArray(m.score) && m.score.length === 2 && m.score.every(Number.isFinite))) {
82
+ delete m.score;
83
+ }
84
+ for (const key of ['created', 'steps', 'bytes']) {
85
+ if (key in m && !(typeof m[key] === 'string' || Number.isFinite(m[key]))) {
86
+ delete m[key];
87
+ }
88
+ }
89
+ for (const key of ['provenance', 'group']) {
90
+ if (key in m && !isObject(m[key])) {
91
+ delete m[key];
92
+ }
93
+ }
94
+ return m;
95
+ }
96
+
97
+ // The title of a recording, a summary, a header or a live source, in the page's language. A
98
+ // title that the server made comes with its message, a shipped sample is known by its id, and any
99
+ // other title reads as titleText reads it, with its scenario in words. `id` is the recording's
100
+ // identifier when the object itself holds none, as a header does.
101
+ export function recordingTitle(item, id = null) {
102
+ if (!item || typeof item !== 'object') {
103
+ return '';
104
+ }
105
+ if (typeof item.title === 'string' && isNameMessage(item.title_id)) {
106
+ return serverText(item, 'title', isNameMessage);
107
+ }
108
+ const sample = Object.prototype.hasOwnProperty.call(SAMPLES, id || item.id) ? SAMPLES[id || item.id] : null;
109
+ if (sample && item.title === sample[0]) {
110
+ return tOr(sample[1], item.title);
111
+ }
112
+ return titleText(item.title, item.scenario);
113
+ }
@@ -0,0 +1,110 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The structure of the page for a screen reader and the browser. Each page has a title of its own
5
+ // in the window, such as Help · Cuju, and Watch takes the title of the recording that plays. The
6
+ // main region takes the name of the page in view, a heading names each page that lacks one of its
7
+ // own, and a change of page is said aloud. A page whose own visible title is its h1, as Teams,
8
+ // Tournaments and Help have, keeps that heading alone, so every page has one h1 and the headings
9
+ // below it descend a level at a time. The skip link moves the focus into the page and leaves
10
+ // the address as it stands. The names at the top of the file touch nothing of the page, so they
11
+ // hold the same in a test. Each name is the message structure.page.<page>.
12
+
13
+ import { t } from './i18n.js';
14
+
15
+ // The pages in the order of the top bar, each opened by the address #page=<page>.
16
+ export const PAGES = ['home', 'watch', 'play', 'challenges', 'recordings', 'matches', 'tournaments', 'teams', 'field', 'help'];
17
+
18
+ export const PAGE_NAMES = Object.fromEntries(PAGES.map((page) => [page, t(`structure.page.${page}`)]));
19
+
20
+ // The name of a page as the page shows it.
21
+ export function pageName(page) {
22
+ return PAGE_NAMES[page] || (page ? `${page[0].toUpperCase()}${page.slice(1)}` : '');
23
+ }
24
+
25
+ // The title of the window on a page. Home is Cuju alone, Watch names the recording that plays,
26
+ // and every other page names itself.
27
+ export function pageTitle(page, watchTitle = null) {
28
+ if (!page || page === 'home') {
29
+ return t('structure.name');
30
+ }
31
+ if (page === 'watch' && watchTitle) {
32
+ return t('structure.title', { title: watchTitle });
33
+ }
34
+ return t('structure.title', { title: pageName(page) });
35
+ }
36
+
37
+ export class PageStructure {
38
+ constructor(app) {
39
+ this.app = app;
40
+ this.watchTitle = null;
41
+ this.main = document.getElementById('pages');
42
+ // One heading for the page in view, kept for a screen reader. A page with an h1 of its
43
+ // own keeps that one, and the heading stays away there.
44
+ this.heading = document.createElement('h1');
45
+ this.heading.id = 'page-name';
46
+ this.heading.className = 'sr-only';
47
+ this.live = document.createElement('p');
48
+ this.live.className = 'sr-only';
49
+ this.live.setAttribute('aria-live', 'polite');
50
+ this.live.id = 'page-live';
51
+ if (this.main) {
52
+ this.main.prepend(this.heading);
53
+ this.main.after(this.live);
54
+ // Pages build their parts as they show, so the heading looks again when they change.
55
+ let queued = false;
56
+ new MutationObserver(() => {
57
+ if (!queued) {
58
+ queued = true;
59
+ requestAnimationFrame(() => {
60
+ queued = false;
61
+ this.placeHeading();
62
+ });
63
+ }
64
+ }).observe(this.main, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden'] });
65
+ }
66
+ const skip = document.querySelector('.skip-link');
67
+ if (skip) {
68
+ skip.addEventListener('click', (e) => {
69
+ e.preventDefault();
70
+ this.main?.focus();
71
+ });
72
+ }
73
+ this.update(app.page, false);
74
+ }
75
+
76
+ // The title of the recording that plays on Watch, or null without one.
77
+ setWatchTitle(title) {
78
+ this.watchTitle = title || null;
79
+ document.title = pageTitle(this.app.page, this.watchTitle);
80
+ }
81
+
82
+ // Shows the heading of the page in view unless the page shows an h1 of its own.
83
+ placeHeading() {
84
+ const section = document.getElementById(`page-${this.app.page}`);
85
+ const own = section && [...section.querySelectorAll('h1')].some((node) => node.getClientRects().length > 0);
86
+ const hidden = this.app.page === 'home' || !!own;
87
+ if (this.heading.hidden !== hidden) {
88
+ this.heading.hidden = hidden;
89
+ }
90
+ }
91
+
92
+ // Names the page in view. With say, the change is also said aloud, unless the focus moved
93
+ // into the page, where the name of the main region already says it.
94
+ update(page, say = true) {
95
+ const name = pageName(page);
96
+ document.title = pageTitle(page, this.watchTitle);
97
+ this.heading.textContent = name;
98
+ this.placeHeading();
99
+ if (this.main) {
100
+ this.main.setAttribute('aria-label', name);
101
+ }
102
+ if (say && !(this.main && this.main.contains(document.activeElement))) {
103
+ // A fresh text each time, so the same page named twice is read twice.
104
+ this.live.textContent = '';
105
+ setTimeout(() => {
106
+ this.live.textContent = t('structure.live', { name });
107
+ }, 50);
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,109 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The scenarios that the page lists, and a short text for each. The texts take their facts from
5
+ // the scenario files and the scenarios page of the docs. The GRF MARL training variants and GRF's
6
+ // example of several agents stay playable from Python and the API, and the page lists leave them
7
+ // out.
8
+
9
+ import { formatNumber, has, plainText, setValueKind, t, tOr } from './i18n.js';
10
+
11
+ const HIDDEN = /^(benchmark_full_game_|test_)/;
12
+
13
+ // The scenarios with a name and a text of their own, whose messages are scenario.<name>.name and
14
+ // scenario.<name>.text. The text of a drill ends on the sentences of its end and its difficulty,
15
+ // the placeholders end and level, and the end of an academy episode is a goal, the ball out of
16
+ // play or a change of possession, else the number of steps of the drill.
17
+ const NAMED = [
18
+ '11_vs_11_stochastic', '11_vs_11_easy_stochastic', '11_vs_11_hard_stochastic', '11_vs_11_competition',
19
+ '11_vs_11_kaggle', '5_vs_5', '1_vs_1_easy', 'academy_empty_goal_close', 'academy_empty_goal',
20
+ 'academy_run_to_score', 'academy_run_to_score_with_keeper', 'academy_pass_and_shoot_with_keeper',
21
+ 'academy_run_pass_and_shoot_with_keeper', 'academy_3_vs_1_with_keeper', 'academy_corner',
22
+ 'academy_counterattack_easy', 'academy_counterattack_hard', 'academy_single_goal_versus_lazy',
23
+ ];
24
+ const ACADEMY_STEPS = { academy_single_goal_versus_lazy: 3001 };
25
+
26
+ function textOf(name) {
27
+ const id = `scenario.${name}.text`;
28
+ if (!has(id)) {
29
+ return '';
30
+ }
31
+ return t(id, {
32
+ end: t('scenario.academy.end', { steps: formatNumber(ACADEMY_STEPS[name] || 401) }),
33
+ level: t('scenario.academy.level'),
34
+ });
35
+ }
36
+
37
+ export const SCENARIO_TEXT = Object.fromEntries(NAMED.map((name) => [name, textOf(name)]));
38
+
39
+ // The agents of a name such as 5_vs_5_l4_r0, in words. The built-in AI playing both teams, l0 r0,
40
+ // says nothing, since the teams of a recording already name who plays.
41
+ function agentsText(left, right) {
42
+ const count = (n) => t('scenario.agents.count', { count: n });
43
+ if (!left && !right) {
44
+ return '';
45
+ }
46
+ if (!right) {
47
+ return t('scenario.agents.left', { agents: count(left) });
48
+ }
49
+ if (!left) {
50
+ return t('scenario.agents.right', { agents: count(right) });
51
+ }
52
+ return left === right ? t('scenario.agents.aSide', { agents: count(left) }) : t('scenario.agents.both', { agents: count(left), right });
53
+ }
54
+
55
+ // The name of a scenario as the page shows it. A scenario without its own name, such as a
56
+ // training variant or a custom file, reads in words, with its underscores as spaces. The suffix
57
+ // of agents on each side, as in 5_vs_5_l0_r0, reads in words after the scenario's name. A name
58
+ // that is no text, as in a malformed file, reads as plainText reads it.
59
+ export function scenarioName(value) {
60
+ const name = plainText(value);
61
+ if (!name) {
62
+ return '';
63
+ }
64
+ if (has(`scenario.${name}.name`)) {
65
+ return t(`scenario.${name}.name`);
66
+ }
67
+ const agents = /^(.+)_l(\d+)_r(\d+)$/.exec(name);
68
+ if (agents) {
69
+ const words = agentsText(Number(agents[2]), Number(agents[3]));
70
+ const base = scenarioName(agents[1]);
71
+ return words ? t('scenario.agents.join', { base, words }) : base;
72
+ }
73
+ const marl = /^benchmark_full_game_(\d+)_vs_(\d+)_(easy|medium|hard)$/.exec(name);
74
+ if (marl) {
75
+ return tOr('scenario.marl.name', `GRF MARL ${marl[1]} vs ${marl[2]}, ${marl[3]}`, {
76
+ left: marl[1], right: marl[2], level: tOr(`scenario.marl.level.${marl[3]}`, marl[3]),
77
+ });
78
+ }
79
+ const words = name.replace(/_/g, ' ').trim();
80
+ return tOr(`scenario.${name}.name`, words.charAt(0).toUpperCase() + words.slice(1));
81
+ }
82
+
83
+ // A title as the page shows it. Titles that a batch or a recorder made by itself start with the
84
+ // name of their scenario, which reads in the language of the page here, so older recordings show the
85
+ // same. A title or a scenario that is no text reads as plainText reads it.
86
+ export function titleText(titleValue, scenarioValue) {
87
+ const title = plainText(titleValue);
88
+ const scenario = plainText(scenarioValue);
89
+ if (!title) {
90
+ return '';
91
+ }
92
+ if (scenario && title.startsWith(scenario)) {
93
+ return scenarioName(scenario) + title.slice(scenario.length);
94
+ }
95
+ return title.replace(/^([a-z0-9]+(?:_[a-z0-9]+)+)(?=,|$)/, (name) => scenarioName(name));
96
+ }
97
+
98
+ // The names a page list shows, in the order given.
99
+ export function listedScenarios(names) {
100
+ return names.filter((name) => !HIDDEN.test(name));
101
+ }
102
+
103
+ // The short text of a scenario, empty for one without a text.
104
+ export function scenarioText(name) {
105
+ return SCENARIO_TEXT[name] || textOf(name);
106
+ }
107
+
108
+ // A scenario that a message of the server names shows by its name on the page.
109
+ setValueKind('scenario', (name) => scenarioName(name));
@@ -0,0 +1,236 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Watching a tournament without spoilers. A person turns it on for one tournament, and the
5
+ // browser remembers it with the matches the person has watched or revealed, by their keys. While
6
+ // it is on, nothing that tells the outcome of an unwatched match shows, neither in the tournament
7
+ // nor in the lists of recordings, Home, the palette and the search of moments. The tournament
8
+ // itself comes from the server as the person sees it, through `POST /api/tournaments/ID/spoilers`,
9
+ // and the recordings of its unwatched matches lose their scores, teams and titles here. The
10
+ // store holds plain data, so the module works alike on the page and in a Node test.
11
+
12
+ import { serverText, t } from './i18n.js';
13
+
14
+ const KEY = 'cuju.spoilers';
15
+ // The most tournaments the browser remembers, the oldest forgotten first.
16
+ const MOST = 200;
17
+
18
+ let memory = null;
19
+
20
+ function storage() {
21
+ try {
22
+ return typeof localStorage === 'undefined' ? null : localStorage;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ // Every tournament the browser remembers, by identifier, each with whether it is on, the keys
29
+ // of the watched matches, the recordings of its matches by key and the predictions by key.
30
+ function load() {
31
+ if (memory) {
32
+ return memory;
33
+ }
34
+ memory = {};
35
+ try {
36
+ const raw = storage()?.getItem(KEY);
37
+ const parsed = raw ? JSON.parse(raw) : null;
38
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
39
+ for (const [id, entry] of Object.entries(parsed)) {
40
+ if (entry && typeof entry === 'object') {
41
+ memory[id] = {
42
+ on: entry.on === true,
43
+ watched: Array.isArray(entry.watched) ? entry.watched.filter((k) => typeof k === 'string') : [],
44
+ recordings: entry.recordings && typeof entry.recordings === 'object' ? { ...entry.recordings } : {},
45
+ predictions: entry.predictions && typeof entry.predictions === 'object' ? { ...entry.predictions } : {},
46
+ autoplay: entry.autoplay === true,
47
+ touched: Number(entry.touched) || 0,
48
+ };
49
+ }
50
+ }
51
+ }
52
+ } catch {
53
+ // A store that cannot be read starts empty, and the page works without it.
54
+ }
55
+ return memory;
56
+ }
57
+
58
+ function save() {
59
+ const all = load();
60
+ const ids = Object.keys(all).sort((a, b) => all[b].touched - all[a].touched);
61
+ for (const id of ids.slice(MOST)) {
62
+ delete all[id];
63
+ }
64
+ try {
65
+ storage()?.setItem(KEY, JSON.stringify(all));
66
+ } catch {
67
+ // A full or blocked store keeps the state for this visit alone.
68
+ }
69
+ }
70
+
71
+ function entry(id, make = false) {
72
+ const all = load();
73
+ if (!all[id] && make) {
74
+ all[id] = { on: false, watched: [], recordings: {}, predictions: {}, autoplay: false, touched: 0 };
75
+ }
76
+ if (all[id] && make) {
77
+ all[id].touched = Date.now();
78
+ }
79
+ return all[id] || null;
80
+ }
81
+
82
+ // Forgets what the module read, so a test or another tab's change reads the store again.
83
+ export function reloadSpoilers() {
84
+ memory = null;
85
+ }
86
+
87
+ // Whether a tournament hides its outcomes until they are watched.
88
+ export function spoilerFree(id) {
89
+ const own = entry(id);
90
+ return !!(own && own.on);
91
+ }
92
+
93
+ export function setSpoilerFree(id, on) {
94
+ entry(id, true).on = !!on;
95
+ save();
96
+ }
97
+
98
+ // The keys of the matches of a tournament that the person has watched or revealed.
99
+ export function watchedKeys(id) {
100
+ const own = entry(id);
101
+ return own ? [...own.watched] : [];
102
+ }
103
+
104
+ export function markWatched(id, ...keys) {
105
+ const own = entry(id, true);
106
+ for (const key of keys) {
107
+ if (typeof key === 'string' && !own.watched.includes(key)) {
108
+ own.watched.push(key);
109
+ }
110
+ }
111
+ save();
112
+ }
113
+
114
+ // Whether the next match plays by itself after a short countdown when one ends.
115
+ export function autoplay(id) {
116
+ const own = entry(id);
117
+ return !!(own && own.autoplay);
118
+ }
119
+
120
+ export function setAutoplay(id, on) {
121
+ entry(id, true).autoplay = !!on;
122
+ save();
123
+ }
124
+
125
+ // Keeps the recordings of a tournament's unwatched matches by key, so a list that holds only a
126
+ // recording's identifier, such as the hits of a search, knows to leave it out.
127
+ export function noteRecordings(id, byKey) {
128
+ const own = entry(id, true);
129
+ let changed = false;
130
+ for (const [key, recording] of Object.entries(byKey || {})) {
131
+ if (typeof recording === 'string' && own.recordings[key] !== recording) {
132
+ own.recordings[key] = recording;
133
+ changed = true;
134
+ }
135
+ }
136
+ if (changed) {
137
+ save();
138
+ }
139
+ }
140
+
141
+ // The prediction of a match, `home`, `draw` or `away`, or null.
142
+ export function prediction(id, key) {
143
+ const own = entry(id);
144
+ return own && ['home', 'draw', 'away'].includes(own.predictions[key]) ? own.predictions[key] : null;
145
+ }
146
+
147
+ export function predict(id, key, pick) {
148
+ const own = entry(id, true);
149
+ if (['home', 'draw', 'away'].includes(pick)) {
150
+ own.predictions[key] = pick;
151
+ } else {
152
+ delete own.predictions[key];
153
+ }
154
+ save();
155
+ }
156
+
157
+ // The outcome of a score as a prediction names it.
158
+ export function outcome(score) {
159
+ if (!Array.isArray(score) || score.length !== 2) {
160
+ return null;
161
+ }
162
+ return score[0] > score[1] ? 'home' : score[0] < score[1] ? 'away' : 'draw';
163
+ }
164
+
165
+ // The predictions of a tournament scored against the watched matches, the right ones and the
166
+ // ones that can be scored, a match counting once its result shows.
167
+ export function predictionScore(id, matches) {
168
+ const own = entry(id);
169
+ let right = 0;
170
+ let scored = 0;
171
+ for (const match of matches || []) {
172
+ const pick = own ? own.predictions[match.key] : null;
173
+ if (!pick || !match.result || !Array.isArray(match.result.score)) {
174
+ continue;
175
+ }
176
+ scored += 1;
177
+ if (outcome(match.result.score) === pick) {
178
+ right += 1;
179
+ }
180
+ }
181
+ return { right, scored };
182
+ }
183
+
184
+ // Whether a recording, by the provenance its header or summary keeps, tells the outcome of a
185
+ // match that the person has not watched yet.
186
+ export function hidesOutcome(provenance) {
187
+ const cup = provenance && provenance.tournament;
188
+ if (!cup || typeof cup.id !== 'string' || typeof cup.key !== 'string') {
189
+ return false;
190
+ }
191
+ const own = entry(cup.id);
192
+ return !!(own && own.on && !own.watched.includes(cup.key));
193
+ }
194
+
195
+ // Whether the recording of this identifier belongs to an unwatched match of a tournament that
196
+ // hides its outcomes.
197
+ export function hiddenRecording(recording) {
198
+ for (const own of Object.values(load())) {
199
+ if (!own.on) {
200
+ continue;
201
+ }
202
+ for (const [key, id] of Object.entries(own.recordings)) {
203
+ if (id === recording && !own.watched.includes(key)) {
204
+ return true;
205
+ }
206
+ }
207
+ }
208
+ return false;
209
+ }
210
+
211
+ // A summary of a recording as a list shows it, without its score, its teams and its title where
212
+ // it tells the outcome of an unwatched match, so the title names only its tournament.
213
+ export function hideSummary(summary) {
214
+ if (!summary || !(hidesOutcome(summary.provenance) || hiddenRecording(summary.id))) {
215
+ return summary;
216
+ }
217
+ const cup = (summary.provenance && summary.provenance.tournament) || {};
218
+ const name = cup.name ? serverText(cup, 'name') : '';
219
+ const provenance = { ...summary.provenance, tournament: { id: cup.id, key: cup.key, name: cup.name, name_id: cup.name_id, name_values: cup.name_values } };
220
+ return {
221
+ ...summary,
222
+ title: name ? t('spoilers.recording.title', { name }) : t('spoilers.recording.untitled'),
223
+ title_id: undefined,
224
+ title_values: undefined,
225
+ teams: [],
226
+ team_labels: [],
227
+ score: null,
228
+ provenance,
229
+ spoiler: true,
230
+ };
231
+ }
232
+
233
+ // A list of summaries with the outcomes of unwatched matches hidden.
234
+ export function hideSummaries(list) {
235
+ return Array.isArray(list) ? list.map(hideSummary) : list;
236
+ }
@@ -0,0 +1,38 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The search of a menu by typing, as in the lists of the system. Letters typed within a short
5
+ // pause build a word, and the focus goes to the first item from the current one whose label starts
6
+ // with it, whatever its case and accents, so typing e finds Équipe and sev finds Several Five. The
7
+ // same letter typed again moves on to the next item that starts with it. The module touches no
8
+ // page, so the Node tests run it alone.
9
+
10
+ // The pause in milliseconds after which a typed letter starts a new word.
11
+ export const PAUSE = 700;
12
+
13
+ // Lower case without accents, so a typed word finds a name however it is written.
14
+ export function fold(text) {
15
+ return String(text ?? '').normalize('NFD').replace(/\p{M}/gu, '').toLocaleLowerCase();
16
+ }
17
+
18
+ // The index of the item that a typed word reaches among the labels, from the item at `current`,
19
+ // or -1 when no label starts with it. A word of one letter, or of one letter typed again and
20
+ // again, moves past the current item, and a longer word keeps the current item while it matches.
21
+ export function typedMatch(labels, current, typed) {
22
+ const word = fold(typed).trimStart();
23
+ const count = labels.length;
24
+ if (!word || !count) {
25
+ return -1;
26
+ }
27
+ const letters = [...word];
28
+ const repeated = letters.every((letter) => letter === letters[0]);
29
+ const search = repeated ? letters[0] : word;
30
+ const from = repeated ? current + 1 : Math.max(current, 0);
31
+ for (let k = 0; k < count; k++) {
32
+ const i = (((from + k) % count) + count) % count;
33
+ if (fold(labels[i]).trimStart().startsWith(search)) {
34
+ return i;
35
+ }
36
+ }
37
+ return -1;
38
+ }