Skip to content

codingame_tools.puzzle_manager

puzzle_manager

Local working-directory management for solving existing CodinGame puzzles--much simpler than codingame_tools.contribution_manager (which authors/edits a contribution): exactly one file (data/solution.src) is ever editable, so there is no git repository involved at all.

See CgPuzzleManager for import_/repair/diff/discard_local/submit/play/play_local/ status/delete--its module docstring covers the three-way state-split design in full. play/play_local each have a resolve_*/*_one pair too (resolve_play_indices/ play_one, resolve_play_local_test_cases/play_local_one), for a caller (e.g. cg puzzle play-server/cg puzzle play's own CLI handlers) that wants to act on each result as it comes in rather than waiting for the whole batch. codingame_tools.puzzle_manager.schema for the working directory's three manifest files (CgPuzzleIdentity/CgPuzzleServerData/CgPuzzleData); codingame_tools.puzzle_manager.statement_render for rendering the cached HTML statement (CgPuzzleManager.load_statement_html) into display blocks, used by cg puzzle description; codingame_tools.test_runner (a separate, package-agnostic package--not puzzle_manager's own) for how play_local/VS Code debugging actually run a solution; and codingame_tools.puzzle_manager.resolver for how a puzzle directory is located.

DATA_SUBDIR_NAME module-attribute

DATA_SUBDIR_NAME = 'data'

The puzzle's user-editable content (solution.src, puzzle-data.json) lives under a data/ subdirectory of the working directory root.

META_SUBDIR_NAME module-attribute

META_SUBDIR_NAME = '.meta'

Container for gitignored, server-derived cache (puzzle-server-data.json) and read-only reference files (statement.html, stub_generator.cgstub)--none of it is user-managed state, and none of it is expected to survive a fresh git clone into a different repo (see CgPuzzleManager.repair, which reconstructs it from puzzle.json's stable puzzle_id). Always paired with a .gitignore (see GITIGNORE_FILE_NAME) at the working directory root, so it's never accidentally tracked by whatever project ends up tracking the rest of the working directory.

SOLUTION_FILE_STEM module-attribute

SOLUTION_FILE_STEM = 'solution'

Stem of the one real, editable/submittable solution file, which lives in data/.

STATEMENT_FILE_NAME module-attribute

STATEMENT_FILE_NAME = 'statement.html'

Read-only reference copy of the puzzle's rendered problem statement (see CgTestSessionQuestionDetails.statement), under .meta/--not user-managed state, so it doesn't belong in data/; regenerated on every import_()/repair(), never read back or diffed; purely for the solver's own convenience (e.g. to reread the problem without a network round trip).

STUB_GENERATOR_FILE_NAME module-attribute

STUB_GENERATOR_FILE_NAME = 'stub_generator.cgstub'

Read-only reference copy of the puzzle's stub-generation script (see CgTestSessionQuestionDetails.stub_generator), under .meta/--informational only; this package doesn't interpret the stub-generator DSL to produce a real starter solution.src, unlike codingame_tools.contribution_manager's Python-only trivial stub for authoring a new contribution (see CgPuzzleManager.import_'s docstring).

TESTS_SUBDIR_NAME module-attribute

TESTS_SUBDIR_NAME = 'tests'

Name of the puzzle working directory's .meta/-relative test-cases subdirectory.

CG_PUZZLE_DIR_ENV_VAR module-attribute

CG_PUZZLE_DIR_ENV_VAR = 'CG_PUZZLE_DIR'

Environment variable that can override puzzle-dir discovery, same as an explicit --puzzle-dir CLI flag (parsing/wiring that flag is the CLI layer's job--this module just accepts the resolved explicit value).

DEFAULT_PUZZLE_SUBDIR_NAME module-attribute

DEFAULT_PUZZLE_SUBDIR_NAME = 'puzzle'

Name of the subdirectory of the current directory checked as a last-resort discovery step.

PUZZLE_IDENTITY_FILE_NAME module-attribute

PUZZLE_IDENTITY_FILE_NAME = 'puzzle.json'

Name of the puzzle working directory's identity/manifest file, at its root (a sibling of data//.meta/)--its presence is what identifies a directory as a puzzle working directory at all.

PUZZLE_SCHEMA_VERSION module-attribute

PUZZLE_SCHEMA_VERSION = 1

Current on-disk format version for a puzzle working directory, recorded in CgPuzzleIdentity.schema_version so a future format change can detect and offer to migrate an older working directory.

CgSubmissionReport dataclass

CgSubmissionReport(validator_shareable, extra_data=dict(), best_score=None, codingamer_id=None, submission_id=None, score=None, achievements_completed=None, shared=None, puzzle_progress=None, validators=None, achievements=None, feedback=None, _completed_time=Alias('completedTime', default=None))

Bases: JSONWizardX

The complete response to Report/findReportBySubmission: a report on a single puzzle submission's results.

CAUTION, confirmed live (2026-07-31, and again 2026-08-01 for a puzzle with no prior submission at all--see best_score): calling findReportBySubmission right after TestSession/submit can race server-side grading--every field below except validator_shareable has been observed entirely absent (not merely null) in some partial-report snapshot. All of them are therefore Optional here except validator_shareable, the only field confirmed present in every observed case so far; a report is only "done" once they're all populated. See CgReportServiceHelper.find_report_by_submission_when_ready, which polls until that's true (or a timeout elapses) instead of returning a partial report.

validator_shareable instance-attribute

validator_shareable

Whether this submission's validator results are eligible to be shared.

best_score class-attribute instance-attribute

best_score = None

The codingamer's best-ever validator score for this puzzle, 0.0 to 100.0 (may be higher than score if this submission wasn't their best attempt). Confirmed live (2026-08-01) absent--not just this submission's own score, but this field specifically--for a puzzle the codingamer had never before submitted, i.e. there's no historical "best" yet at the moment this was polled. Absent while grading is still in progress--see the class docstring.

codingamer_id class-attribute instance-attribute

codingamer_id = None

The submitting codingamer's numeric ID. Absent while grading is still in progress--see the class docstring.

submission_id class-attribute instance-attribute

submission_id = None

Numeric ID of the submission this report is for (matches the submission_id argument). Absent while grading is still in progress--see the class docstring.

score class-attribute instance-attribute

score = None

The submission's validator score, 0.0 to 100.0. Absent while grading is still in progress--see the class docstring.

achievements_completed class-attribute instance-attribute

achievements_completed = None

Whether all achievements for this puzzle have been completed by the codingamer. Absent while grading is still in progress--see the class docstring.

shared class-attribute instance-attribute

shared = None

Whether the codingamer has publicly shared their solution. Absent while grading is still in progress--see the class docstring.

puzzle_progress class-attribute instance-attribute

puzzle_progress = None

Lightweight puzzle progress summary. Absent while grading is still in progress--see the class docstring.

validators class-attribute instance-attribute

validators = None

Per-validator results for this submission. Absent while grading is still in progress--see the class docstring.

achievements class-attribute instance-attribute

achievements = None

Achievements unlocked by this submission. Only observed as an empty list so far, so element shape is unknown otherwise. Absent while grading is still in progress--see the class docstring.

feedback class-attribute instance-attribute

feedback = None

Community feedback/rating summary for the puzzle. Not confirmed to always be present (only a single example observed so far).

completed_time property writable

completed_time

See the field docstring for _completed_time. Always UTC. None if not yet done.

is_ready

is_ready()

Whether grading has finished--i.e. every field described as "absent while grading is still in progress" above is now populated. See the class docstring and CgReportServiceHelper.find_report_by_submission_when_ready.

Source code in codingame_tools/client/common/protocol/report.py
140
141
142
143
144
145
146
147
148
149
150
151
def is_ready(self) -> bool:
    """Whether grading has finished--i.e. every field described as "absent while grading is
       still in progress" above is now populated. See the class docstring and
       `CgReportServiceHelper.find_report_by_submission_when_ready`."""
    return (
            self.best_score is not None
            and self.codingamer_id is not None and self.submission_id is not None
            and self.score is not None and self.achievements_completed is not None
            and self.shared is not None and self.puzzle_progress is not None
            and self.validators is not None and self.achievements is not None
            and self._completed_time is not None
        )

CgPuzzleBuildFailedError

CgPuzzleBuildFailedError(result)

Bases: CgPuzzleManagerError

Raised by CgPuzzleManager.play_local when build_solution() failed, so no test case was run at all. Carries the full CgBuildResult (compiler diagnostics in .result.output) via .result.

Note build_solution() itself does not raise this--it returns the result, so a caller driving the build directly can display diagnostics however it likes. This exists for the batch wrapper, which has no other way to say "nothing ran".

Source code in codingame_tools/puzzle_manager/manager.py
258
259
260
def __init__(self, result: CgBuildResult) -> None:
    self.result = result
    super().__init__(f"solution failed to build:\n{result.output}")

CgPuzzleDiscardResult dataclass

CgPuzzleDiscardResult(code, solution_language)

The outcome of CgPuzzleManager.discard_local().

code instance-attribute

code

The server's last-submitted code, now also written to data/solution.src.

solution_language instance-attribute

solution_language

The language code is written in (the server's last submission may be in a different language than data/puzzle-data.json's previously-recorded solution_language--this is the fresh, now-authoritative value; discard_local() updates puzzle-data.json to match).

CgPuzzleLocalTestFailedError

CgPuzzleLocalTestFailedError(results)

Bases: CgPuzzleManagerError

Raised by CgPuzzleManager.play_local if any test case failed. Carries every result (not just the failing ones) via .results, so a caller can report the full picture.

Source code in codingame_tools/puzzle_manager/manager.py
242
243
244
245
246
def __init__(self, results: list[CgPuzzleLocalTestResult]) -> None:
    self.results = results
    failed = [r for r in results if not r.passed]
    summary = ", ".join(f"#{r.index} ({r.label})" for r in failed)
    super().__init__(f"{len(failed)}/{len(results)} local test case(s) failed: {summary}")

CgPuzzleLocalTestResult dataclass

CgPuzzleLocalTestResult(index, label, passed, input, expected_output, actual_output, stderr, timed_out)

The outcome of running data/solution.src against one downloaded .meta/tests/ test case--see CgPuzzleManager.play_local.

index instance-attribute

index

The test case's server-assigned index (see CgPuzzleDownloadedTestCase.index).

label instance-attribute

label

The test case's real label.

passed instance-attribute

passed

Whether the run completed without crashing/timing out and its stdout matched the test case's expected output (see codingame_tools.test_runner.outputs_match).

input instance-attribute

input

The test case's input, exactly as fed to the solution's stdin.

expected_output instance-attribute

expected_output

The test case's expected output (output.txt).

actual_output instance-attribute

actual_output

What the solution actually wrote to stdout.

stderr instance-attribute

stderr

What the solution wrote to stderr (not itself a failure condition, but useful context when a test does fail).

timed_out instance-attribute

timed_out

Whether the run was killed for exceeding its timeout rather than running to completion.

CgPuzzleManager

CgPuzzleManager(puzzle_dir, client, *, toolchain_dir=None, mount_root=None, toolchain_languages=None, toolchain_image=None)

Builds/updates a puzzle working directory (puzzle_dir) against the server, via an already-authenticated CgClient. See the module docstring for the (deliberately much simpler than codingame_tools.contribution_manager) design this is backed by.

Source code in codingame_tools/puzzle_manager/manager.py
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
def __init__(
            self,
            puzzle_dir: Path | str,
            client: CgClient,
            *,
            toolchain_dir: Path | None = None,
            mount_root: Path | None = None,
            toolchain_languages: list[str] | None = None,
            toolchain_image: str | None = None,
        ) -> None:
    self.puzzle_dir = Path(puzzle_dir).resolve()
    self.client = client
    self.mount_root = Path(mount_root).resolve() if mount_root is not None else None
    self.toolchain_languages = toolchain_languages
    self.toolchain_image = toolchain_image
    self.toolchain_dir = (
            Path(toolchain_dir) if toolchain_dir is not None
            else default_global_data_dir() / TOOLCHAIN_SUBDIR_NAME
        )

toolchain_dir instance-attribute

toolchain_dir = Path(toolchain_dir) if toolchain_dir is not None else default_global_data_dir() / TOOLCHAIN_SUBDIR_NAME

Per-user global directory holding user-tweakable per-language toolchain (container image) definitions--see codingame_tools.language.CgLanguageContext.toolchain_dir. Global rather than per-working-directory so one tweak applies everywhere. The CLI passes the value resolved from config; the default keeps library/test use working with no config at all.

mount_root instance-attribute

mount_root = Path(mount_root).resolve() if mount_root is not None else None

Editor workspace root to bind-mount for containerized languages, or None to derive it (see language_context). Normally VS Code's ${workspaceFolder}, passed through by the CLI: cg's own find_workspace_root is a heuristic, and the editor knows the real answer.

identity_file property

identity_file

Path to this working directory's puzzle.json (stable identity) manifest.

server_data_file property

server_data_file

Path to this working directory's .meta/puzzle-server-data.json (gitignored cache).

tests_dir property

tests_dir

Path to this working directory's .meta/tests/ (downloaded test case input/output--see codingame_tools.puzzle_manager.test_cases_dir).

data_dir property

data_dir

Path to this working directory's data/ subdirectory.

solution_file property

solution_file

The one real solution file, data/solution.<ext>.

Resolved by looking for whatever is actually there rather than by deriving the name from the recorded language: a working directory written by an older cg still has solution.src, and the file that exists is the one the user has been editing.

solution_snapshot_file property

solution_snapshot_file

Path to .meta/solution-snapshot.json--see CgPuzzleSolutionSnapshot.

selected_test_file property

selected_test_file

Path to .meta/selected-test.json--see CgPuzzleSelectedTest.

puzzle_data_file property

puzzle_data_file

Path to this working directory's data/puzzle-data.json (user-editable metadata).

statement_file property

statement_file

Path to this working directory's .meta/statement.html (read-only reference copy of the puzzle's rendered problem statement).

load_selected_test

load_selected_test()

The explicitly selected test case, or None if none has been chosen.

Source code in codingame_tools/puzzle_manager/manager.py
462
463
464
465
466
def load_selected_test(self) -> CgPuzzleSelectedTest | None:
    """The explicitly selected test case, or None if none has been chosen."""
    if not self.selected_test_file.is_file():
        return None
    return CgPuzzleSelectedTest.load(self.selected_test_file)

select_test

select_test(test_index)

Choose which test case the debugger runs against.

Raises:

  • CgPuzzleManagerError

    if no downloaded test case has that index--catching a typo now rather than at the moment a debug session fails to start.

Source code in codingame_tools/puzzle_manager/manager.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
def select_test(self, test_index: int) -> None:
    """Choose which test case the debugger runs against.

    Raises:
        CgPuzzleManagerError: if no downloaded test case has that index--catching a typo now
                               rather than at the moment a debug session fails to start.
    """
    available = [tc.index for tc in list_downloaded_test_cases(self.tests_dir)]
    if test_index not in available:
        raise CgPuzzleManagerError(
                f"No downloaded test case with index {test_index}. "
                f"Available: {', '.join(str(i) for i in available) or '(none--run `cg puzzle repair`)'}.")
    self.meta_dir.mkdir(parents=True, exist_ok=True)
    CgPuzzleSelectedTest(test_index=test_index).save(self.selected_test_file)

clear_selected_test

clear_selected_test()

Forget the explicit selection, falling back to the default (the first test case).

Source code in codingame_tools/puzzle_manager/manager.py
483
484
485
def clear_selected_test(self) -> None:
    """Forget the explicit selection, falling back to the default (the first test case)."""
    self.selected_test_file.unlink(missing_ok=True)

resolve_debug_test_index

resolve_debug_test_index()

Which single test a debug session should use: the selection, else the first test case.

Defaulting rather than refusing is deliberate--debugging works immediately after an import, with no selection step, which is the common case.

Raises:

Source code in codingame_tools/puzzle_manager/manager.py
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
def resolve_debug_test_index(self) -> int:
    """Which single test a debug session should use: the selection, else the first test case.

       Defaulting rather than refusing is deliberate--debugging works immediately after an
       import, with no selection step, which is the common case.

    Raises:
        CgPuzzleManagerError: if there are no downloaded test cases at all.
    """
    downloaded = list_downloaded_test_cases(self.tests_dir)
    if not downloaded:
        raise CgPuzzleManagerError(
                f"No downloaded test cases in {self.tests_dir}--run `cg puzzle repair` first.")
    selected = self.load_selected_test()
    if selected is not None and any(tc.index == selected.test_index for tc in downloaded):
        return selected.test_index
    return downloaded[0].index

load_identity

load_identity()

Load puzzle.json, or None if this directory has never been imported.

Source code in codingame_tools/puzzle_manager/manager.py
518
519
520
521
522
def load_identity(self) -> CgPuzzleIdentity | None:
    """Load `puzzle.json`, or None if this directory has never been imported."""
    if not self.identity_file.is_file():
        return None
    return CgPuzzleIdentity.load(self.identity_file)

load_statement_html

load_statement_html()

Read .meta/statement.html, or None if it doesn't exist (never imported, or .meta/ needs repair()).

Source code in codingame_tools/puzzle_manager/manager.py
524
525
526
527
528
529
def load_statement_html(self) -> str | None:
    """Read `.meta/statement.html`, or None if it doesn't exist (never imported, or `.meta/`
       needs `repair()`)."""
    if not self.statement_file.is_file():
        return None
    return file_to_server_text(self.statement_file.read_text(encoding="utf-8"))

load_server_data

load_server_data()

Load .meta/puzzle-server-data.json, or None if it's missing (needs repair()--e.g. a fresh clone that (correctly) didn't bring gitignored .meta/ along).

Source code in codingame_tools/puzzle_manager/manager.py
531
532
533
534
535
536
def load_server_data(self) -> CgPuzzleServerData | None:
    """Load `.meta/puzzle-server-data.json`, or None if it's missing (needs `repair()`--e.g.
       a fresh clone that (correctly) didn't bring gitignored `.meta/` along)."""
    if not self.server_data_file.is_file():
        return None
    return CgPuzzleServerData.load(self.server_data_file)

load_puzzle_data

load_puzzle_data()

Load data/puzzle-data.json, or None if this directory has never been imported.

Source code in codingame_tools/puzzle_manager/manager.py
538
539
540
541
542
def load_puzzle_data(self) -> CgPuzzleData | None:
    """Load `data/puzzle-data.json`, or None if this directory has never been imported."""
    if not self.puzzle_data_file.is_file():
        return None
    return CgPuzzleData.load(self.puzzle_data_file)

load_solution

load_solution()

Read data/solution.src.

Raises:

  • FileNotFoundError

    if solution.src doesn't exist.

Source code in codingame_tools/puzzle_manager/manager.py
569
570
571
572
573
574
575
def load_solution(self) -> str:
    """Read `data/solution.src`.

    Raises:
        FileNotFoundError: if `solution.src` doesn't exist.
    """
    return file_to_server_text(self.solution_file.read_text(encoding="utf-8"))

import_ async

import_(puzzle_ref, *, language=None)
resolves puzzle_ref to a real

pretty ID (see _resolve_puzzle_ref--a numeric ID, a pretty ID, an exact title match, or a case-insensitive title match, tried in that order), then resolves this codingamer's test session for it (Puzzle/generateSessionFromPuzzlePrettyId), then TestSession/startTestSession to fetch its current state.

What lands in data/solution.src depends on language:

  • language=None (the default): the codingamer's existing saved answer, in whatever language they last used (CgTestSessionQuestion.answer), or a placeholder in _DEFAULT_IMPORT_LANGUAGE if this puzzle has never been attempted at all.
  • language given: that language, seeded with the codingamer's most recent saved code for it (CodinGame keeps one per language--see CgTestSessionService.get_previous_code_by_language_id), or a placeholder if they've never attempted this puzzle in it. Equivalent to importing and then calling set_language(), and it shares that code path.

A placeholder is a bare comment: this package does not interpret the puzzle's stub-generator DSL to produce a real starter solution the way an IDE would; .meta/stub_generator.cgstub (see below) is written as a read-only reference instead, for the solver to consult by hand.

Also writes .meta/statement.html, .meta/stub_generator.cgstub, and .meta/tests/ (each test case's downloaded input/output--see codingame_tools.puzzle_manager.test_cases_dir)--all read-only reference copies, regenerated here, never read back or diffed--and refreshes the solution.<ext> convenience symlink at the working directory root--see the module docstring for why these live under .meta/ rather than data/.

Parameters:

  • puzzle_ref (str) –

    A general puzzle reference--numeric ID, pretty ID, exact title, or case-insensitive title (see _resolve_puzzle_ref).

  • language (CgSolutionLanguage | None, default: None ) –

    Language to start in. Defaults to None, meaning "whichever language the codingamer last used for this puzzle". When given, switches to it and restores any code already saved in it--see above.

Raises:

  • CgPuzzleManagerError

    if this directory already tracks a puzzle, if puzzle_ref couldn't be resolved to a real puzzle, or if the puzzle isn't a supported type (currently, only classic "PUZZLE_INOUT" puzzles).

Source code in codingame_tools/puzzle_manager/manager.py
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
async def import_(
            self,
            puzzle_ref: str,
            *,
            language: CgSolutionLanguage | None = None,
        ) -> CgPuzzleData:
    """Build this working directory from an existing puzzle: resolves `puzzle_ref` to a real
       pretty ID (see `_resolve_puzzle_ref`--a numeric ID, a pretty ID, an exact title match,
       or a case-insensitive title match, tried in that order), then resolves this
       codingamer's test session for it (`Puzzle/generateSessionFromPuzzlePrettyId`), then
       `TestSession/startTestSession` to fetch its current state.

       What lands in `data/solution.src` depends on `language`:

       - **`language=None`** (the default): the codingamer's existing saved answer, in whatever
         language they last used (`CgTestSessionQuestion.answer`), or a placeholder in
         `_DEFAULT_IMPORT_LANGUAGE` if this puzzle has never been attempted at all.
       - **`language` given**: that language, seeded with the codingamer's most recent saved
         code *for it* (CodinGame keeps one per language--see
         `CgTestSessionService.get_previous_code_by_language_id`), or a placeholder if they've
         never attempted this puzzle in it. Equivalent to importing and then calling
         `set_language()`, and it shares that code path.

       A placeholder is a bare comment: this package does not interpret the puzzle's
       stub-generator DSL to produce a real starter solution the way an IDE would;
       `.meta/stub_generator.cgstub` (see below) is written as a read-only reference instead,
       for the solver to consult by hand.

       Also writes `.meta/statement.html`, `.meta/stub_generator.cgstub`, and `.meta/tests/`
       (each test case's downloaded input/output--see
       `codingame_tools.puzzle_manager.test_cases_dir`)--all read-only reference copies,
       regenerated here, never read back or diffed--and refreshes the `solution.<ext>`
       convenience symlink at the working directory root--see the module docstring for why
       these live under `.meta/` rather than `data/`.

    Args:
        puzzle_ref: A general puzzle reference--numeric ID, pretty ID, exact title, or
                    case-insensitive title (see `_resolve_puzzle_ref`).
        language:   Language to start in. Defaults to `None`, meaning "whichever language the
                    codingamer last used for this puzzle". When given, switches to it and
                    restores any code already saved in it--see above.

    Raises:
        CgPuzzleManagerError: if this directory already tracks a puzzle, if `puzzle_ref`
                               couldn't be resolved to a real puzzle, or if the puzzle isn't a
                               supported type (currently, only classic "PUZZLE_INOUT"
                               puzzles).
    """
    if self.load_identity() is not None:
        raise CgPuzzleManagerError(
                f"{self.identity_file} already exists--this working directory has already "
                "been imported."
            )

    puzzle_pretty_id = await self._resolve_puzzle_ref(puzzle_ref)
    test_session_handle = await self.client.services.puzzle.generate_session_from_puzzle_pretty_id(
            puzzle_pretty_id)
    session = await self.client.services.test_session.start_test_session(test_session_handle)
    question = session.current_question.question
    # `contribution` is absent for a puzzle CodinGame itself provides (confirmed live
    # 2026-08-02 with "Temperatures"), since an official puzzle was never a community
    # contribution--so its contribution type is simply unknowable. Treat that as a standard
    # in/out puzzle rather than refusing: this check exists to reject *known* unsupported kinds,
    # and failing closed here would block importing every official puzzle on the site.
    contribution_type = (
            question.contribution.contribution_type if question.contribution is not None else None)
    if contribution_type is not None and contribution_type != _SUPPORTED_CONTRIBUTION_TYPE:
        raise CgPuzzleManagerError(
                f"Puzzle {puzzle_pretty_id!r} is a {contribution_type!r} puzzle--only "
                f"{_SUPPORTED_CONTRIBUTION_TYPE!r} puzzles are supported so far."
            )

    answer = session.current_question.answer
    # `answer` itself can be non-None (an empty placeholder object) even with no solution
    # ever submitted--`code`/`programming_language_id` are the actual "has a real answer"
    # signal; see CgTestSessionAnswer's docstring.
    if language is not None:
        # An explicit language means "start in this one", not merely "use it if there's nothing
        # saved"--so fetch the codingamer's own most recent code for it, exactly as
        # `set_language()` would. Without this, asking for a language you'd previously written
        # a solution in would silently discard that solution in favor of a placeholder.
        solution_language = language
        saved = await self.client.services.test_session.get_previous_code_by_language_id(
                test_session_handle, language)
        solution_code = saved if saved is not None else _placeholder_solution(
                language, question.title, puzzle_pretty_id)
    elif answer is not None and answer.code is not None and answer.programming_language_id is not None:
        solution_language = answer.programming_language_id
        solution_code = answer.code
    else:
        solution_language = _DEFAULT_IMPORT_LANGUAGE
        solution_code = _placeholder_solution(
                solution_language, question.title, puzzle_pretty_id)

    self.data_dir.mkdir(parents=True, exist_ok=True)
    self.meta_dir.mkdir(parents=True, exist_ok=True)
    self._write_solution(solution_code, solution_language)
    (self.meta_dir / STATEMENT_FILE_NAME).write_text(
            server_text_to_file(question.statement), encoding="utf-8")
    (self.meta_dir / STUB_GENERATOR_FILE_NAME).write_text(
            server_text_to_file(question.stub_generator), encoding="utf-8")
    await download_test_cases(self.client, question.test_cases, self.tests_dir)
    _write_meta_gitignore(self.puzzle_dir)

    CgPuzzleIdentity(
            schema_version=PUZZLE_SCHEMA_VERSION, puzzle_id=session.puzzle.id,
            puzzle_handle=session.puzzle.handle,
        ).save(self.identity_file)
    CgPuzzleServerData(
            test_session_handle=test_session_handle, title=question.title,
            puzzle_pretty_id=puzzle_pretty_id, puzzle_type=contribution_type,
            difficulty=session.puzzle.level,
        ).save(self.server_data_file)
    puzzle_data = CgPuzzleData(solution_language=solution_language)
    puzzle_data.save(self.puzzle_data_file)

    _align_solution_file_name(self.puzzle_dir, solution_language)
    return puzzle_data

repair async

repair()

Reconstruct .meta/ (the test session handle, plus the read-only statement.html/ stub_generator.cgstub/tests/ reference copies) from puzzle.json's stable puzzle_id--for recovering from .meta/ being missing, e.g. after a fresh clone into a different repo (it's gitignored on purpose--see the module docstring) or manual deletion/corruption. data/ (solution.src, puzzle-data.json) is never touched-- there's nothing to preserve from, since it's exactly the git-tracked content a clone would have brought along.

Looks up Puzzle/findProgressByIds([puzzle_id]) for a fresh pretty_id/title, and (if already available there) a reusable test_session_handle directly--otherwise falls back to Puzzle/generateSessionFromPuzzlePrettyId using that fresh pretty_id. Either way, cross-checks the resulting session's own reported puzzle ID against puzzle_id before trusting anything else about it (see CgPuzzleServerData's docstring for why a looked-up pretty_id specifically is never trusted un-verified).

Raises:

  • FileNotFoundError

    if this working directory has never been imported (no puzzle.json), or data/solution.src itself is missing (nothing on disk to refresh the solution symlink for/repair alongside).

  • CgPuzzleManagerError

    if .meta/ already exists (nothing to repair), or if a fresh lookup's own reported puzzle ID doesn't match puzzle_id (refuses rather than risk repairing with mismatched data).

Source code in codingame_tools/puzzle_manager/manager.py
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
async def repair(self) -> CgPuzzleServerData:
    """Reconstruct `.meta/` (the test session handle, plus the read-only `statement.html`/
       `stub_generator.cgstub`/`tests/` reference copies) from `puzzle.json`'s stable
       `puzzle_id`--for recovering from `.meta/` being missing, e.g. after a fresh clone into
       a different repo (it's gitignored on purpose--see the module docstring) or manual
       deletion/corruption. `data/` (`solution.src`, `puzzle-data.json`) is never touched--
       there's nothing to preserve *from*, since it's exactly the git-tracked content a clone
       would have brought along.

       Looks up `Puzzle/findProgressByIds([puzzle_id])` for a fresh `pretty_id`/`title`, and
       (if already available there) a reusable `test_session_handle` directly--otherwise
       falls back to `Puzzle/generateSessionFromPuzzlePrettyId` using that fresh `pretty_id`.
       Either way, cross-checks the resulting session's own reported puzzle ID against
       `puzzle_id` before trusting anything else about it (see `CgPuzzleServerData`'s
       docstring for why a looked-up `pretty_id` specifically is never trusted un-verified).

    Raises:
        FileNotFoundError: if this working directory has never been imported (no
                            `puzzle.json`), or `data/solution.src` itself is missing (nothing
                            on disk to refresh the solution symlink for/repair alongside).
        CgPuzzleManagerError: if `.meta/` already exists (nothing to repair), or if a fresh
                               lookup's own reported puzzle ID doesn't match `puzzle_id`
                               (refuses rather than risk repairing with mismatched data).
    """
    identity = self.load_identity()
    if identity is None:
        raise FileNotFoundError(
                f"{self.identity_file} does not exist--this working directory has never "
                "been imported (nothing to repair)."
            )
    if self.server_data_file.is_file():
        raise CgPuzzleManagerError(f"{self.server_data_file} already exists--nothing to repair.")
    if not self.solution_file.is_file():
        raise FileNotFoundError(f"{self.solution_file} does not exist--nothing on disk to repair alongside.")

    progress_results = await self.client.services.puzzle.find_progress_by_ids([identity.puzzle_id])
    if not progress_results or progress_results[0].id != identity.puzzle_id:
        raise CgPuzzleManagerError(
                f"Puzzle/findProgressByIds([{identity.puzzle_id}]) did not return a matching "
                "result--refusing to repair with mismatched data."
            )
    progress = progress_results[0]

    test_session_handle = progress.test_session_handle
    if test_session_handle is None:
        test_session_handle = await self.client.services.puzzle.generate_session_from_puzzle_pretty_id(
                progress.pretty_id)

    session = await self.client.services.test_session.start_test_session(test_session_handle)
    if session.puzzle.id != identity.puzzle_id:
        raise CgPuzzleManagerError(
                f"TestSession/startTestSession({test_session_handle!r}) returned puzzle "
                f"{session.puzzle.id}, expected {identity.puzzle_id}--refusing to repair "
                "with mismatched data."
            )
    question = session.current_question.question

    self.meta_dir.mkdir(parents=True, exist_ok=True)
    (self.meta_dir / STATEMENT_FILE_NAME).write_text(
            server_text_to_file(question.statement), encoding="utf-8")
    (self.meta_dir / STUB_GENERATOR_FILE_NAME).write_text(
            server_text_to_file(question.stub_generator), encoding="utf-8")
    await download_test_cases(self.client, question.test_cases, self.tests_dir)
    _write_meta_gitignore(self.puzzle_dir)

    server_data = CgPuzzleServerData(
            test_session_handle=test_session_handle, title=progress.title,
            puzzle_pretty_id=progress.pretty_id,
            # None for an official CodinGame puzzle, which has no contribution to read a
            # type from--see import_(). CgPuzzleServerData.puzzle_type is already
            # optional, so this stores cleanly.
            puzzle_type=(
                    question.contribution.contribution_type
                    if question.contribution is not None else None),
            difficulty=session.puzzle.level,
        )
    server_data.save(self.server_data_file)

    puzzle_data = self.load_puzzle_data()
    if puzzle_data is not None:
        _align_solution_file_name(self.puzzle_dir, puzzle_data.solution_language)
    return server_data

diff async

diff()

A unified text diff between the local data/solution.src and the server's current last-submitted answer for this puzzle--empty if they're identical, or if there's no local file/no server answer at all yet (nothing meaningful to diff in that case).

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgPuzzleManagerError

    if .meta/ is missing (run repair() first).

Source code in codingame_tools/puzzle_manager/manager.py
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
async def diff(self) -> str:
    """A unified text diff between the local `data/solution.src` and the server's current
       last-submitted answer for this puzzle--empty if they're identical, or if there's no
       local file/no server answer at all yet (nothing meaningful to diff in that case).

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgPuzzleManagerError: if `.meta/` is missing (run `repair()` first).
    """
    _, _, puzzle_data = self._require_state()
    local_lines = file_to_server_text(
            self.solution_file.read_text(encoding="utf-8")).splitlines(keepends=True) \
        if self.solution_file.is_file() else []
    # Compared against the server's code *in the local language*, not against whatever language
    # the test session happens to be sitting in. CodinGame stores a puzzle's code per language,
    # so the session's answer can easily be a different language entirely -- diffing a local C++
    # file against a saved Python one produced a whole-file diff that meant nothing.
    language = puzzle_data.solution_language
    saved = await self._fetch_saved_code_for_language(language) if language else None
    server_lines = saved.splitlines(keepends=True) if saved is not None else []
    return "".join(difflib.unified_diff(server_lines, local_lines, fromfile="server", tofile="local"))

load_solution_snapshot

load_solution_snapshot()

What this client last wrote to data/solution.src, or None if unknown (never written, or .meta/ predates the snapshot).

Source code in codingame_tools/puzzle_manager/manager.py
913
914
915
916
917
918
def load_solution_snapshot(self) -> CgPuzzleSolutionSnapshot | None:
    """What this client last wrote to `data/solution.src`, or `None` if unknown (never
       written, or `.meta/` predates the snapshot)."""
    if not self.solution_snapshot_file.is_file():
        return None
    return CgPuzzleSolutionSnapshot.load(self.solution_snapshot_file)

set_language async

set_language(language, *, force=False)

Switch this working directory to a different language, restoring the codingamer's own most recent code for it.

CodinGame keeps your latest source per language for a puzzle, so switching is not "throw away what you have and start over"--any solution you'd previously written in the target language comes back (see CgTestSessionService.get_previous_code_by_language_id). Only a language you have never attempted gets a placeholder.

This changes local state only. The server's notion of your current language is not moved by fetching code (confirmed live--it's a pure read); it follows once you actually run a server-side test or submit in the new language.

Refuses when data/solution.src holds work the server doesn't have, since switching overwrites it. Local edits are considered safe to discard when they match either the server's saved code for the current language or the placeholder this package would have generated for it--the latter matters because importing with an explicit language you've never used writes a placeholder that was never saved server-side, which would otherwise leave the working directory permanently unable to switch away.

Parameters:

  • language (CgSolutionLanguage) –

    CodinGame language ID to switch to, e.g. "C++" (see CgSolutionLanguage).

  • force (bool, default: False ) –

    Switch even when local edits would be lost.

Returns:

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgPuzzleManagerError

    if language isn't one this client knows, if it's already the current language, or if local edits would be lost and force is False.

Source code in codingame_tools/puzzle_manager/manager.py
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
async def set_language(
            self,
            language: CgSolutionLanguage,
            *,
            force: bool = False,
        ) -> CgPuzzleSetLanguageResult:
    """Switch this working directory to a different language, restoring the codingamer's own
       most recent code for it.

       CodinGame keeps your latest source *per language* for a puzzle, so switching is not
       "throw away what you have and start over"--any solution you'd previously written in the
       target language comes back (see
       `CgTestSessionService.get_previous_code_by_language_id`). Only a language you have never
       attempted gets a placeholder.

       **This changes local state only.** The server's notion of your current language is not
       moved by fetching code (confirmed live--it's a pure read); it follows once you actually
       run a server-side test or submit in the new language.

       Refuses when `data/solution.src` holds work the server doesn't have, since switching
       overwrites it. Local edits are considered safe to discard when they match either the
       server's saved code for the current language *or* the placeholder this package would
       have generated for it--the latter matters because importing with an explicit language
       you've never used writes a placeholder that was never saved server-side, which would
       otherwise leave the working directory permanently unable to switch away.

    Args:
        language: CodinGame language ID to switch to, e.g. "C++" (see `CgSolutionLanguage`).
        force:    Switch even when local edits would be lost.

    Returns:
        A `CgPuzzleSetLanguageResult`--check `from_server` to tell "your old solution is back"
        from "here's an empty starting point".

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgPuzzleManagerError: if `language` isn't one this client knows, if it's already the
                               current language, or if local edits would be lost and `force`
                               is False.
    """
    _, server_data, puzzle_data = self._require_state()
    previous_language = puzzle_data.solution_language
    if language not in list_language_cg_ids():
        raise CgPuzzleManagerError(
                f"{language!r} isn't a language this client knows. Known languages: "
                f"{', '.join(list_language_cg_ids())}."
            )
    if language == previous_language:
        raise CgPuzzleManagerError(
                f"{self.puzzle_dir} is already using {language!r}--nothing to switch."
            )

    test_session = self.client.services.test_session
    if not force and not await self._solution_is_safe_to_replace(server_data, previous_language):
        raise CgPuzzleManagerError(
                f"{self.solution_file} has {previous_language!r} changes the server doesn't "
                "have--switching would discard them. Submit them first (`cg puzzle submit`), "
                "or pass --force to discard them."
            )

    saved_new = await test_session.get_previous_code_by_language_id(
            server_data.test_session_handle, language)
    from_server = saved_new is not None
    code = saved_new if saved_new is not None else _placeholder_solution(
            language, server_data.title or "", server_data.puzzle_pretty_id or "")

    self._write_solution(code, language)
    dataclasses.replace(puzzle_data, solution_language=language).save(self.puzzle_data_file)
    _align_solution_file_name(self.puzzle_dir, language)
    return CgPuzzleSetLanguageResult(
            language=language, previous_language=previous_language,
            code=code, from_server=from_server,
        )

discard_local async

discard_local()
overwrite data/solution.src with the server's current

last-submitted answer for this puzzle (and update data/puzzle-data.json's solution_language to match, in case the last submission was in a different language than previously recorded), then refresh the solution.<ext> symlink. Purely a local overwrite--no submission or other server-side side effect.

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgPuzzleManagerError

    if .meta/ is missing (run repair() first), or if this puzzle has never been submitted at all (nothing server-side to discard to).

Source code in codingame_tools/puzzle_manager/manager.py
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
async def discard_local(self) -> CgPuzzleDiscardResult:
    """Discard local edits: overwrite `data/solution.src` with the server's current
       last-submitted answer for this puzzle (and update `data/puzzle-data.json`'s
       `solution_language` to match, in case the last submission was in a different language
       than previously recorded), then refresh the `solution.<ext>` symlink. Purely a local
       overwrite--no submission or other server-side side effect.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgPuzzleManagerError: if `.meta/` is missing (run `repair()` first), or if this
                               puzzle has never been submitted at all (nothing server-side to
                               discard to).
    """
    identity, server_data, puzzle_data = self._require_state()
    current = await self._fetch_current_answer_code()
    if current is None:
        raise CgPuzzleManagerError(
                f"Puzzle {identity.puzzle_id} has no server-side answer yet (never "
                "submitted)--nothing to discard local edits to."
            )
    code, solution_language = current
    self._write_solution(code, solution_language)
    if solution_language != puzzle_data.solution_language:
        dataclasses.replace(puzzle_data, solution_language=solution_language).save(self.puzzle_data_file)
    _align_solution_file_name(self.puzzle_dir, solution_language)
    return CgPuzzleDiscardResult(code=code, solution_language=solution_language)

submit async

submit()

Submit the current local data/solution.src to the server for credit (TestSession/submit), in data/puzzle-data.json's recorded solution_language, then fetch and return the resulting results report (Report/findReportBySubmission)--score, achievement completion, and per-validator pass/fail.

Named submit(), not push() (unlike codingame_tools.contribution_manager's git-vocabulary naming)--a puzzle working directory has two distinct server-side persistence phases, not one: the test session's current answer (see play()'s docstring--confirmed live to be silently updated by any TestSession/play call, not just this method) and this method's actual graded submission. "Push" would suggest the former; this method is unambiguously the latter.

CAUTION: unlike codingame_tools.contribution_manager's push(), this always creates a new graded submission--there's no draft/private-staging concept for puzzle solutions. See CgTestSessionService.submit's docstring for the (currently unhandled) heavy-validation Cloudflare/524 timeout risk shared with contribution submission.

The report is fetched via CgReportServiceHelper.find_report_by_submission_when_ready rather than the plain find_report_by_submission, since calling the latter immediately after submitting can race server-side grading--see CgSubmissionReport's class docstring.

Returns:

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgPuzzleManagerError

    if .meta/ is missing (run repair() first).

  • TimeoutError

    if grading hasn't finished within find_report_by_submission_when_ready's default timeout.

Source code in codingame_tools/puzzle_manager/manager.py
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
async def submit(self) -> CgSubmissionReport:
    """Submit the current local `data/solution.src` to the server for credit
       (`TestSession/submit`), in `data/puzzle-data.json`'s recorded `solution_language`,
       then fetch and return the resulting results report
       (`Report/findReportBySubmission`)--score, achievement completion, and per-validator
       pass/fail.

       Named `submit()`, not `push()` (unlike `codingame_tools.contribution_manager`'s
       git-vocabulary naming)--a puzzle working directory has two distinct server-side
       persistence phases, not one: the test session's current answer (see `play()`'s
       docstring--confirmed live to be silently updated by *any* `TestSession/play` call, not
       just this method) and this method's actual graded submission. "Push" would suggest
       the former; this method is unambiguously the latter.

       CAUTION: unlike `codingame_tools.contribution_manager`'s `push()`, this always
       creates a new graded submission--there's no draft/private-staging concept for puzzle
       solutions. See `CgTestSessionService.submit`'s docstring for the (currently
       unhandled) heavy-validation Cloudflare/524 timeout risk shared with contribution
       submission.

       The report is fetched via `CgReportServiceHelper.find_report_by_submission_when_ready`
       rather than the plain `find_report_by_submission`, since calling the latter immediately
       after submitting can race server-side grading--see `CgSubmissionReport`'s class
       docstring.

    Returns:
        The new submission's `CgSubmissionReport` (its `.submission_id` is the same numeric
        ID `TestSession/submit` itself returns).

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgPuzzleManagerError: if `.meta/` is missing (run `repair()` first).
        TimeoutError: if grading hasn't finished within
                      `find_report_by_submission_when_ready`'s default timeout.
    """
    _, server_data, puzzle_data = self._require_state()
    code = file_to_server_text(self.solution_file.read_text(encoding="utf-8"))
    request = CgSubmitRequest(code=code, programming_language_id=puzzle_data.solution_language)
    submission_id = await self.client.services.test_session.submit(server_data.test_session_handle, request)
    return await self.client.services.report.helper.find_report_by_submission_when_ready(submission_id)

resolve_play_indices

resolve_play_indices(test_indices=None)

Resolve which 1-based test indices play()/play_one() should run against: test_indices if given, unchanged; otherwise every downloaded test case's index (.meta/tests/, i.e. every test case this working directory actually knows about--NOT necessarily every test case the puzzle has). No network access--for a caller that wants to loop over play_one() itself (e.g. to display each result as it comes in, rather than waiting for the whole batch--see play()), this is the piece that used to be done implicitly inside play().

Raises:

  • FileNotFoundError

    if this working directory has never been imported, or (only when test_indices is not given) has no downloaded test cases at all.

  • CgPuzzleManagerError

    if .meta/ is missing (run repair() first).

Source code in codingame_tools/puzzle_manager/manager.py
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
def resolve_play_indices(self, test_indices: list[int] | None = None) -> list[int]:
    """Resolve which 1-based test indices `play()`/`play_one()` should run against:
       `test_indices` if given, unchanged; otherwise every downloaded test case's index
       (`.meta/tests/`, i.e. every test case this working directory actually knows about--NOT
       necessarily every test case the puzzle has). No network access--for a caller that wants
       to loop over `play_one()` itself (e.g. to display each result as it comes in, rather
       than waiting for the whole batch--see `play()`), this is the piece that used to be
       done implicitly inside `play()`.

    Raises:
        FileNotFoundError: if this working directory has never been imported, or (only when
                            `test_indices` is not given) has no downloaded test cases at all.
        CgPuzzleManagerError: if `.meta/` is missing (run `repair()` first).
    """
    self._require_state()
    if test_indices is not None:
        return test_indices
    downloaded = list_downloaded_test_cases(self.tests_dir)
    if not downloaded:
        raise FileNotFoundError(f"{self.tests_dir} has no downloaded test cases--run `cg puzzle repair` first.")
    return [tc.index for tc in downloaded]

play_one async

play_one(index)

Run the current local data/solution.src against a single one of the puzzle's test cases via the server (TestSession/play--the IDE's "Test"/"Run" button, as opposed to submit()'s full "Submit"). One live API call.

CONFIRMED LIVE (2026-08-01): this call has a side effect beyond just running the given test case--the server durably persists whatever code was sent as the test session's current answer (the same "current answer" returned by TestSession/startTestSession, and visible in the web IDE from any browser), whether or not the test case actually passes. This is NOT a grading/submission event (no Report/score is produced), and there's no separate "just save, don't run" call--the web IDE itself has no autosave either (confirmed: editing code there without running a test, then navigating away, prompts "All changes will be lost")--so running at least one test case is, in effect, the only way to persist a change short of a real submission. submit() also persists the code this way (again regardless of whether the submission scores well), as a side effect of grading it.

Parameters:

  • index (int) –

    1-based index to run against (see CgTestSessionTestCase.index). Need not be locally downloaded--the server runs by index alone.

Returns:

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgPuzzleManagerError

    if .meta/ is missing (run repair() first).

Source code in codingame_tools/puzzle_manager/manager.py
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
async def play_one(self, index: int) -> CgPuzzleRemoteTestResult:
    """Run the current local `data/solution.src` against a single one of the puzzle's test
       cases via the server (`TestSession/play`--the IDE's "Test"/"Run" button, as opposed
       to `submit()`'s full "Submit"). One live API call.

       CONFIRMED LIVE (2026-08-01): this call has a side effect beyond just running the given
       test case--the server durably persists whatever `code` was sent as the test session's
       current answer (the same "current answer" returned by `TestSession/startTestSession`,
       and visible in the web IDE from any browser), whether or not the test case actually
       passes. This is NOT a grading/submission event (no `Report`/score is produced), and
       there's no separate "just save, don't run" call--the web IDE itself has no autosave
       either (confirmed: editing code there without running a test, then navigating away,
       prompts "All changes will be lost")--so running at least one test case is, in effect,
       the only way to persist a change short of a real submission. `submit()` also persists
       the code this way (again regardless of whether the submission scores well), as a side
       effect of grading it.

    Args:
        index: 1-based index to run against (see `CgTestSessionTestCase.index`). Need not be
               locally downloaded--the server runs by index alone.

    Returns:
        The `CgPuzzleRemoteTestResult` for this index.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgPuzzleManagerError: if `.meta/` is missing (run `repair()` first).
    """
    _, server_data, puzzle_data = self._require_state()
    downloaded = list_downloaded_test_cases(self.tests_dir)
    labels_by_index = {tc.index: tc.label for tc in downloaded}
    code = file_to_server_text(self.solution_file.read_text(encoding="utf-8"))
    request = CgPlayRequest(
            code=code,
            programming_language_id=puzzle_data.solution_language,
            multiple_languages=CgMultipleLanguagesTestParams(test_index=index),
        )
    play_result = await self.client.services.test_session.play(server_data.test_session_handle, request)
    return CgPuzzleRemoteTestResult(
            index=index, label=labels_by_index.get(index, f"test {index}"), result=play_result,
        )

play async

play(test_indices=None)

Run the current local data/solution.src against one or more of the puzzle's test cases via the server (TestSession/play). Convenience batch wrapper around play_one()--each index is a separate live API call (there is no batch form of TestSession/play), run sequentially, in the order given; see play_one()'s docstring for the shared side-effect caveat.

A caller that wants to display/act on each result as soon as it's available, rather than waiting for every index to finish first, should call resolve_play_indices() and play_one() directly in its own loop instead of this method (see cg puzzle play-server's CLI implementation for exactly that).

Parameters:

  • test_indices (list[int] | None, default: None ) –

    1-based indices to run against (see CgTestSessionTestCase.index). Need not be locally downloaded--the server runs by index alone. If not given, runs every downloaded test case (.meta/tests/)--see resolve_play_indices().

Returns:

Raises:

  • FileNotFoundError

    if this working directory has never been imported, or (only when test_indices is not given) has no downloaded test cases at all.

  • CgPuzzleManagerError

    if .meta/ is missing (run repair() first).

Source code in codingame_tools/puzzle_manager/manager.py
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
async def play(self, test_indices: list[int] | None = None) -> list[CgPuzzleRemoteTestResult]:
    """Run the current local `data/solution.src` against one or more of the puzzle's test
       cases via the server (`TestSession/play`). Convenience batch wrapper around
       `play_one()`--each index is a separate live API call (there is no batch form of
       `TestSession/play`), run sequentially, in the order given; see `play_one()`'s docstring
       for the shared side-effect caveat.

       A caller that wants to display/act on each result as soon as it's available, rather
       than waiting for every index to finish first, should call `resolve_play_indices()` and
       `play_one()` directly in its own loop instead of this method (see `cg puzzle
       play-server`'s CLI implementation for exactly that).

    Args:
        test_indices: 1-based indices to run against (see `CgTestSessionTestCase.index`).
                      Need not be locally downloaded--the server runs by index alone.
                      If not given, runs every downloaded test case (`.meta/tests/`)--see
                      `resolve_play_indices()`.

    Returns:
        One `CgPuzzleRemoteTestResult` per index, in the order run.

    Raises:
        FileNotFoundError: if this working directory has never been imported, or (only when
                            `test_indices` is not given) has no downloaded test cases at all.
        CgPuzzleManagerError: if `.meta/` is missing (run `repair()` first).
    """
    indices = self.resolve_play_indices(test_indices)
    return [await self.play_one(index) for index in indices]

resolve_play_local_test_cases

resolve_play_local_test_cases(test_indices=None)

Resolve which downloaded test cases play_local()/play_local_one() should run against: the downloaded test cases matching test_indices, in the order given, if given; otherwise every downloaded test case (.meta/tests/). No subprocess execution --for a caller that wants to loop over play_local_one() itself (e.g. to display each result as it comes in, rather than waiting for the whole batch--see play_local()), this is the piece that used to be done implicitly inside play_local().

Raises:

  • FileNotFoundError

    if this working directory has never been imported, or has no downloaded test cases at all (run cg puzzle repair first).

  • CgPuzzleManagerError

    if test_indices contains an index with no downloaded test case.

Source code in codingame_tools/puzzle_manager/manager.py
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
def resolve_play_local_test_cases(
            self,
            test_indices: list[int] | None = None,
        ) -> list[CgPuzzleDownloadedTestCase]:
    """Resolve which downloaded test cases `play_local()`/`play_local_one()` should run
       against: the downloaded test cases matching `test_indices`, in the order given, if
       given; otherwise every downloaded test case (`.meta/tests/`). No subprocess execution
       --for a caller that wants to loop over `play_local_one()` itself (e.g. to display each
       result as it comes in, rather than waiting for the whole batch--see `play_local()`),
       this is the piece that used to be done implicitly inside `play_local()`.

    Raises:
        FileNotFoundError: if this working directory has never been imported, or has no
                            downloaded test cases at all (run `cg puzzle repair` first).
        CgPuzzleManagerError: if `test_indices` contains an index with no downloaded test
                               case.
    """
    identity = self.load_identity()
    if identity is None:
        raise FileNotFoundError(
                f"{self.identity_file} does not exist--this working directory has never "
                "been imported (see `cg puzzle import`)."
            )
    if self.load_puzzle_data() is None:
        raise FileNotFoundError(f"{self.puzzle_data_file} does not exist--this working directory is in an inconsistent state.")
    downloaded = list_downloaded_test_cases(self.tests_dir)
    if not downloaded:
        raise FileNotFoundError(f"{self.tests_dir} has no downloaded test cases--run `cg puzzle repair` first.")
    if test_indices is None:
        return downloaded
    by_index = {tc.index: tc for tc in downloaded}
    test_cases: list[CgPuzzleDownloadedTestCase] = []
    for index in test_indices:
        test_case = by_index.get(index)
        if test_case is None:
            raise CgPuzzleManagerError(f"No downloaded test case with index {index}.")
        test_cases.append(test_case)
    return test_cases

language_context

language_context(solution_language=None, *, mount_root=None)

Describe this working directory to codingame_tools.language--see CgLanguageContext.

mount_root is what a containerized language bind-mounts. It defaults to the enclosing VS Code workspace root, so that in-container paths equal host paths and one container serves the whole workspace; pass it explicitly (VS Code's ${workspaceFolder}) when the real workspace is known, since find_workspace_root is only a guess.

Infallible by design: never reads puzzle.json, never needs the directory to have been imported. solution_language is accepted for signature stability but no longer selects a path: there is one real solution file and solution_file finds it whatever extension it carries.

Source code in codingame_tools/puzzle_manager/manager.py
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
def language_context(
            self,
            solution_language: CgSolutionLanguage | None = None,
            *,
            mount_root: Path | None = None,
        ) -> CgLanguageContext:
    """Describe this working directory to `codingame_tools.language`--see `CgLanguageContext`.

       `mount_root` is what a containerized language bind-mounts. It defaults to the enclosing
       VS Code workspace root, so that in-container paths equal host paths and one container
       serves the whole workspace; pass it explicitly (VS Code's `${workspaceFolder}`) when the
       real workspace is known, since `find_workspace_root` is only a guess.

       Infallible by design: never reads `puzzle.json`, never needs the directory to have been
       imported. `solution_language` is accepted for signature stability but no longer selects
       a path: there is one real solution file and `solution_file` finds it whatever extension
       it carries.
    """
    return CgLanguageContext(
            root=self.puzzle_dir,
            solution_file=self.solution_file,
            meta_dir=self.meta_dir,
            toolchain_dir=self.toolchain_dir,
            mount_root=mount_root or self.mount_root or find_workspace_root(self.puzzle_dir),
            toolchain_languages=self.toolchain_languages,
            toolchain_image=self.toolchain_image,
        )

provision_vscode async

provision_vscode(*, workspace_root=None, force=False, check=False, debug_adapter_logging=False)

Generate this working directory's VS Code run/debug configuration, if its language has any, and write it into the workspace.

What's generated is the same for every working directory of that language, so this is run once per language rather than once per directory, and nothing here goes stale when test cases or the solution language change.

Parameters:

  • workspace_root (Path | None, default: None ) –

    Where .vscode/ goes. Defaults to find_workspace_root()--VS Code reads launch.json only from the workspace root, which is often not this working directory (see codingame_tools.language.vscode).

  • force (bool, default: False ) –

    Overwrite an existing config file that isn't strict JSON (i.e. uses JSONC comments) instead of refusing.

  • debug_adapter_logging (bool, default: False ) –
            Generate a configuration that logs the debug adapter's
             own protocol exchange--see `CgVsCodeRequest`.
    
  • check (bool, default: False ) –

    Report what would change without touching anything. This is how staleness is detected: generated entries carry no version stamp, so "would rewriting change anything?" is the whole question, and it stays correct when a future release alters what gets generated.

Returns:

  • list[Path]

    Every path that changed (or, under check, would change), in write order. Empty means

  • list[Path]

    already up to date, or that this language has no VS Code integration.

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgVsCodeMergeError

    if an existing config file can't be safely merged into.

Source code in codingame_tools/puzzle_manager/manager.py
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
async def provision_vscode(
            self,
            *,
            workspace_root: Path | None = None,
            force: bool = False,
            check: bool = False,
            debug_adapter_logging: bool = False,
        ) -> list[Path]:
    """Generate this working directory's VS Code run/debug configuration, if its language has
       any, and write it into the workspace.

       What's generated is the same for every working directory of that language, so this is
       run once per language rather than once per directory, and nothing here goes stale when
       test cases or the solution language change.

    Args:
        workspace_root: Where `.vscode/` goes. Defaults to `find_workspace_root()`--VS Code
                         reads `launch.json` only from the workspace *root*, which is often
                         not this working directory (see `codingame_tools.language.vscode`).
        force:          Overwrite an existing config file that isn't strict JSON (i.e. uses
                         JSONC comments) instead of refusing.
        debug_adapter_logging:
                        Generate a configuration that logs the debug adapter's
                         own protocol exchange--see `CgVsCodeRequest`.
        check:          Report what *would* change without touching anything. This is how
                         staleness is detected: generated entries carry no version stamp, so
                         "would rewriting change anything?" is the whole question, and it stays
                         correct when a future release alters what gets generated.

    Returns:
        Every path that changed (or, under `check`, would change), in write order. Empty means
        already up to date, or that this language has no VS Code integration.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgVsCodeMergeError: if an existing config file can't be safely merged into.
    """
    puzzle_data = self.load_puzzle_data()
    if puzzle_data is None:
        raise FileNotFoundError(f"{self.puzzle_data_file} does not exist--this working directory is in an inconsistent state.")
    resolved_workspace_root = (
            Path(workspace_root).resolve() if workspace_root is not None
            else find_workspace_root(self.puzzle_dir)
        )
    request = CgVsCodeRequest(
            ctx=self.language_context(
                    puzzle_data.solution_language, mount_root=resolved_workspace_root),
            workspace_root=resolved_workspace_root,
            debug_adapter_logging=debug_adapter_logging,
        )
    provisioning = await get_language(puzzle_data.solution_language).build_vscode_provisioning(request)
    if provisioning is None:
        return []
    return write_provisioning(
            provisioning, root=self.puzzle_dir, workspace_root=resolved_workspace_root,
            language=puzzle_data.solution_language, force=force, dry_run=check)

start_debug_session async

start_debug_session(test_index, *, timeout=DEFAULT_BUILD_TIMEOUT_SECONDS)

Get the solution ready for a debugger to attach to, fed by test case test_index's input--see codingame_tools.language.CgLanguage.start_debug_session.

Only meaningful for a language whose debugger attaches to a running target (C++ via gdbserver). Python3's debugger launches the program itself, so it never calls this.

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgPuzzleManagerError

    if there's no downloaded test case with that index.

  • CgLanguageOperationNotSupportedError

    if this language has no attach-style debugging.

Source code in codingame_tools/puzzle_manager/manager.py
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
async def start_debug_session(
            self,
            test_index: int,
            *,
            timeout: float = DEFAULT_BUILD_TIMEOUT_SECONDS,
        ) -> CgDebugSession:
    """Get the solution ready for a debugger to attach to, fed by test case `test_index`'s
       input--see `codingame_tools.language.CgLanguage.start_debug_session`.

       Only meaningful for a language whose debugger attaches to a running target (C++ via
       gdbserver). Python3's debugger launches the program itself, so it never calls this.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgPuzzleManagerError: if there's no downloaded test case with that index.
        CgLanguageOperationNotSupportedError: if this language has no attach-style debugging.
    """
    puzzle_data = self.load_puzzle_data()
    if puzzle_data is None:
        raise FileNotFoundError(f"{self.puzzle_data_file} does not exist--this working directory is in an inconsistent state.")
    test_case = next(
            (tc for tc in list_downloaded_test_cases(self.tests_dir) if tc.index == test_index),
            None,
        )
    if test_case is None:
        raise CgPuzzleManagerError(f"No downloaded test case with index {test_index}.")
    ctx = self.language_context(puzzle_data.solution_language)
    # The downloaded file's bytes verbatim: `.meta/tests/` holds byte-exact fileservlet
    # downloads, so this is already exactly what CodinGame puts on the solution's stdin.
    return await get_language(puzzle_data.solution_language).start_debug_session(
            ctx, test_case.input_text, timeout=timeout)

stop_debug_session async

stop_debug_session()

Tear down whatever start_debug_session() started. Safe to call when nothing is running.

Source code in codingame_tools/puzzle_manager/manager.py
1343
1344
1345
1346
1347
1348
1349
1350
async def stop_debug_session(self) -> None:
    """Tear down whatever `start_debug_session()` started. Safe to call when nothing is
       running."""
    puzzle_data = self.load_puzzle_data()
    if puzzle_data is None:
        return
    ctx = self.language_context(puzzle_data.solution_language)
    await get_language(puzzle_data.solution_language).stop_debug_session(ctx)

build_solution async

build_solution(*, profile='run', timeout=DEFAULT_BUILD_TIMEOUT_SECONDS)

Build data/solution.src for local execution, if its language needs building at all (Python3 doesn't--this is then an immediate no-op success).

A separate step from play_local_one() so a caller can display build diagnostics apart from program output, report a compile error once rather than once per test case, and give building its own generous timeout. Cheap to call repeatedly: an unchanged source since the last successful build returns up_to_date=True having done nothing.

play_local() calls this for you. A caller driving play_local_one() itself (as cg puzzle play does, to stream results) must call this first.

Returns:

  • CgBuildResult

    A CgBuildResult--check .ok; a build failure is reported, never raised.

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

Source code in codingame_tools/puzzle_manager/manager.py
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
async def build_solution(
            self,
            *,
            profile: CgBuildProfile = "run",
            timeout: float = DEFAULT_BUILD_TIMEOUT_SECONDS,
        ) -> CgBuildResult:
    """Build `data/solution.src` for local execution, if its language needs building at all
       (Python3 doesn't--this is then an immediate no-op success).

       A separate step from `play_local_one()` so a caller can display build diagnostics apart
       from program output, report a compile error once rather than once per test case, and give
       building its own generous timeout. Cheap to call repeatedly: an unchanged source since the
       last successful build returns `up_to_date=True` having done nothing.

       `play_local()` calls this for you. A caller driving `play_local_one()` itself (as
       `cg puzzle play` does, to stream results) must call this first.

    Returns:
        A `CgBuildResult`--check `.ok`; a build failure is reported, never raised.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
    """
    puzzle_data = self.load_puzzle_data()
    if puzzle_data is None:
        raise FileNotFoundError(f"{self.puzzle_data_file} does not exist--this working directory is in an inconsistent state.")
    language = get_language(puzzle_data.solution_language)
    ctx = self.language_context(puzzle_data.solution_language)
    return await language.build(ctx, profile=profile, timeout=timeout)

play_local_one async

play_local_one(test_case, *, timeout=DEFAULT_RUN_TIMEOUT_SECONDS)

Run the current local data/solution.src against a single downloaded test case entirely locally--no network access at all, unlike play_one()--by shelling out to the appropriate interpreter/compiler as a subprocess (see codingame_tools.language.CgLanguage.run) and comparing captured stdout to the test case's expected output.txt.

Never raises just because the test failed (crashed, timed out, or mismatched)--that's reflected in the returned result's passed, same spirit as codingame_tools. contribution_manager.manager.CgContributionManager.run_local_test. See play_local(), which raises CgPuzzleLocalTestFailedError if any of a batch failed.

Does not build. For a language that needs compiling, call build_solution() first (play_local() does this for you); this method only runs the already-built artifact.

For stepping through solution.src in a debugger against a specific test case's input instead, see codingame_tools.test_runner.debug_stdin (launched directly, not through this method--a subprocess like this one spawns can't be stepped into).

Parameters:

  • test_case (CgPuzzleDownloadedTestCase) –

    Which downloaded test case to run (see resolve_play_local_test_cases()).

  • timeout (float, default: DEFAULT_RUN_TIMEOUT_SECONDS ) –

    Wall-clock timeout in seconds--see codingame_tools.language. DEFAULT_RUN_TIMEOUT_SECONDS.

Returns:

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgLanguageOperationNotSupportedError

    if data/puzzle-data.json's solution_language isn't yet supported by codingame_tools. language.

Source code in codingame_tools/puzzle_manager/manager.py
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
async def play_local_one(
            self,
            test_case: CgPuzzleDownloadedTestCase,
            *,
            timeout: float = DEFAULT_RUN_TIMEOUT_SECONDS,
        ) -> CgPuzzleLocalTestResult:
    """Run the current local `data/solution.src` against a single downloaded test case
       entirely locally--no network access at all, unlike `play_one()`--by shelling out to
       the appropriate interpreter/compiler as a subprocess (see
       `codingame_tools.language.CgLanguage.run`) and comparing captured stdout to the test
       case's expected `output.txt`.

       Never raises just because the test failed (crashed, timed out, or mismatched)--that's
       reflected in the returned result's `passed`, same spirit as `codingame_tools.
       contribution_manager.manager.CgContributionManager.run_local_test`. See `play_local()`,
       which raises `CgPuzzleLocalTestFailedError` if any of a batch failed.

       **Does not build.** For a language that needs compiling, call `build_solution()` first
       (`play_local()` does this for you); this method only runs the already-built artifact.

       For stepping through `solution.src` in a debugger against a specific test case's input
       instead, see `codingame_tools.test_runner.debug_stdin` (launched directly, not through
       this method--a subprocess like this one spawns can't be stepped into).

    Args:
        test_case: Which downloaded test case to run (see `resolve_play_local_test_cases()`).
        timeout:   Wall-clock timeout in seconds--see `codingame_tools.language.
                   DEFAULT_RUN_TIMEOUT_SECONDS`.

    Returns:
        The outcome--see `CgPuzzleLocalTestResult`.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgLanguageOperationNotSupportedError: if `data/puzzle-data.json`'s `solution_language`
                                               isn't yet supported by `codingame_tools.
                                               language`.
    """
    puzzle_data = self.load_puzzle_data()
    if puzzle_data is None:
        raise FileNotFoundError(f"{self.puzzle_data_file} does not exist--this working directory is in an inconsistent state.")
    ctx = self.language_context(puzzle_data.solution_language)
    run_result = await get_language(puzzle_data.solution_language).run(
            ctx, test_case.input_text, timeout=timeout)
    passed = not run_result.timed_out and run_result.returncode == 0 \
        and outputs_match(run_result.output, test_case.output_text)
    return CgPuzzleLocalTestResult(
            index=test_case.index, label=test_case.label, passed=passed,
            input=test_case.input_text, expected_output=test_case.output_text,
            actual_output=run_result.output, stderr=run_result.stderr,
            timed_out=run_result.timed_out,
        )

play_local async

play_local(test_indices=None, *, timeout=DEFAULT_RUN_TIMEOUT_SECONDS, build_timeout=DEFAULT_BUILD_TIMEOUT_SECONDS)

Run the current local data/solution.src against the downloaded .meta/tests/ test cases entirely locally--no network access at all, unlike play(). Convenience batch wrapper that calls build_solution() once and then loops play_local_one() sequentially, in the order given.

A caller that wants to display/act on each result as soon as it's available, rather than waiting for every test case to finish first, should call build_solution(), resolve_play_local_test_cases() and play_local_one() directly in its own loop instead of this method (see cg puzzle play's CLI implementation for exactly that).

Parameters:

  • test_indices (list[int] | None, default: None ) –

    If given, only run the downloaded test cases with these indices (the same numbering .meta/tests/'s directory names and play()'s own test_indices use), run in the order given. Defaults to running every downloaded test case--see resolve_play_local_test_cases().

  • timeout (float, default: DEFAULT_RUN_TIMEOUT_SECONDS ) –

    Per-test-case wall-clock timeout in seconds--see codingame_tools.language.DEFAULT_RUN_TIMEOUT_SECONDS.

  • build_timeout (float, default: DEFAULT_BUILD_TIMEOUT_SECONDS ) –

    Wall-clock timeout for the one-time build step--see codingame_tools.language.DEFAULT_BUILD_TIMEOUT_SECONDS.

Returns:

Raises:

  • FileNotFoundError

    if this working directory has never been imported, or has no downloaded test cases at all (run cg puzzle repair first).

  • CgPuzzleManagerError

    if test_indices contains an index with no downloaded test case.

  • CgPuzzleBuildFailedError

    if the solution failed to build--carries the build output.

  • CgLanguageOperationNotSupportedError

    if data/puzzle-data.json's solution_language isn't yet supported by codingame_tools. language.

  • CgPuzzleLocalTestFailedError

    if any test case's output didn't match (or the solution crashed/timed out)--carries every result via .results.

Source code in codingame_tools/puzzle_manager/manager.py
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
async def play_local(
            self,
            test_indices: list[int] | None = None,
            *,
            timeout: float = DEFAULT_RUN_TIMEOUT_SECONDS,
            build_timeout: float = DEFAULT_BUILD_TIMEOUT_SECONDS,
        ) -> list[CgPuzzleLocalTestResult]:
    """Run the current local `data/solution.src` against the downloaded `.meta/tests/` test
       cases entirely locally--no network access at all, unlike `play()`. Convenience batch
       wrapper that calls `build_solution()` once and then loops `play_local_one()`
       sequentially, in the order given.

       A caller that wants to display/act on each result as soon as it's available, rather
       than waiting for every test case to finish first, should call `build_solution()`,
       `resolve_play_local_test_cases()` and `play_local_one()` directly in its own loop
       instead of this method (see `cg puzzle play`'s CLI implementation for exactly that).

    Args:
        test_indices: If given, only run the downloaded test cases with these indices (the
                      same numbering `.meta/tests/`'s directory names and `play()`'s own
                      `test_indices` use), run in the order given. Defaults to running every
                      downloaded test case--see `resolve_play_local_test_cases()`.
        timeout:    Per-test-case wall-clock timeout in seconds--see
                    `codingame_tools.language.DEFAULT_RUN_TIMEOUT_SECONDS`.
        build_timeout: Wall-clock timeout for the one-time build step--see
                    `codingame_tools.language.DEFAULT_BUILD_TIMEOUT_SECONDS`.

    Returns:
        One `CgPuzzleLocalTestResult` per test case run, in the order run.

    Raises:
        FileNotFoundError: if this working directory has never been imported, or has no
                            downloaded test cases at all (run `cg puzzle repair` first).
        CgPuzzleManagerError: if `test_indices` contains an index with no downloaded test
                               case.
        CgPuzzleBuildFailedError: if the solution failed to build--carries the build output.
        CgLanguageOperationNotSupportedError: if `data/puzzle-data.json`'s `solution_language`
                                               isn't yet supported by `codingame_tools.
                                               language`.
        CgPuzzleLocalTestFailedError: if any test case's output didn't match (or the solution
                                       crashed/timed out)--carries every result via `.results`.
    """
    test_cases = self.resolve_play_local_test_cases(test_indices)
    build_result = await self.build_solution(timeout=build_timeout)
    if not build_result.ok:
        raise CgPuzzleBuildFailedError(build_result)
    results = [await self.play_local_one(test_case, timeout=timeout) for test_case in test_cases]
    if any(not r.passed for r in results):
        raise CgPuzzleLocalTestFailedError(results)
    return results

status async

status(*, refresh=False)

A point-in-time summary of this working directory--see CgPuzzleStatus.

By default, entirely local/cheap: no network access at all--just the three on-disk manifests. Pass refresh=True to also check local_dirty (a live TestSession/startTestSession call, same as diff()) and fetch progress (a live Puzzle/findProgressByIds call)--both stay None otherwise. Unlike codingame_tools.contribution_manager's status(), there is no cache file this writes to for next time--puzzle working directories have no such cache at all (see the module docstring); every refresh=True call is genuinely live, every time.

Parameters:

  • refresh (bool, default: False ) –

    If True, also check for local edits against the server's last-submitted answer and fetch live progress/score info. Defaults to False.

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

  • CgPuzzleManagerError

    if .meta/ is missing (run repair() first).

Source code in codingame_tools/puzzle_manager/manager.py
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
async def status(self, *, refresh: bool = False) -> CgPuzzleStatus:
    """A point-in-time summary of this working directory--see `CgPuzzleStatus`.

       By default, entirely local/cheap: no network access at all--just the three on-disk
       manifests. Pass `refresh=True` to also check `local_dirty` (a live
       `TestSession/startTestSession` call, same as `diff()`) and fetch `progress` (a live
       `Puzzle/findProgressByIds` call)--both stay `None` otherwise. Unlike
       `codingame_tools.contribution_manager`'s `status()`, there is no cache file this writes
       to for next time--puzzle working directories have no such cache at all (see the module
       docstring); every `refresh=True` call is genuinely live, every time.

    Args:
        refresh: If True, also check for local edits against the server's last-submitted
                 answer and fetch live progress/score info. Defaults to False.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
        CgPuzzleManagerError: if `.meta/` is missing (run `repair()` first).
    """
    identity, server_data, puzzle_data = self._require_state()
    local_dirty: bool | None = None
    progress: CgLastActivityPuzzle | None = None
    if refresh:
        local_dirty = bool(await self.diff())
        progress_results = await self.client.services.puzzle.find_progress_by_ids([identity.puzzle_id])
        if progress_results and progress_results[0].id == identity.puzzle_id:
            progress = progress_results[0]
    return CgPuzzleStatus(
            puzzle_dir=self.puzzle_dir,
            puzzle_id=identity.puzzle_id,
            puzzle_handle=identity.puzzle_handle,
            title=server_data.title,
            puzzle_pretty_id=server_data.puzzle_pretty_id,
            puzzle_type=server_data.puzzle_type,
            difficulty=server_data.difficulty,
            solution_language=puzzle_data.solution_language,
            local_dirty=local_dirty,
            progress=progress,
        )

delete async

delete()

Remove this working directory entirely (puzzle.json, .meta/, data/, and the solution.<ext> convenience symlink)--purely local. Unlike codingame_tools. contribution_manager.CgContributionManager.delete(), there is no server-side counterpart at all here--a puzzle already exists on the server before you can solve it (see the module docstring), so there is nothing to delete there; this only ever removes your own local working directory.

No confirmation prompt here--that's the CLI's job (cg puzzle delete), same as every other method in this class.

Raises:

  • FileNotFoundError

    if this working directory has never been imported.

Source code in codingame_tools/puzzle_manager/manager.py
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
async def delete(self) -> None:
    """Remove this working directory entirely (`puzzle.json`, `.meta/`, `data/`, and the
       `solution.<ext>` convenience symlink)--purely local. Unlike `codingame_tools.
       contribution_manager.CgContributionManager.delete()`, there is no server-side
       counterpart at all here--a puzzle already exists on the server before you can solve
       it (see the module docstring), so there is nothing to delete *there*; this only ever
       removes your own local working directory.

       No confirmation prompt here--that's the CLI's job (`cg puzzle delete`), same as every
       other method in this class.

    Raises:
        FileNotFoundError: if this working directory has never been imported.
    """
    if self.load_identity() is None:
        raise FileNotFoundError(
                f"{self.identity_file} does not exist--this working directory has never "
                "been imported (nothing to delete)."
            )
    # Before removing the directory: a containerized language leaves a long-lived container
    # bind-mounted to it. Orphaning one is worse than untidy--container names are derived from
    # the directory path, so a new working directory later created at the same path would
    # otherwise silently attach to the stale container (and its stale build artifacts).
    await remove_containers_for_root(self.puzzle_dir)
    shutil.rmtree(self.puzzle_dir)

CgPuzzleManagerError

Bases: Exception

Raised for puzzle-manager-level errors not better represented by a more specific exception (e.g. importing an unsupported puzzle type, discarding local edits when nothing has ever been submitted to discard to, or a repair() whose fresh lookup didn't actually match the puzzle it was supposed to repair).

CgPuzzleRemoteTestResult dataclass

CgPuzzleRemoteTestResult(index, label, result)

The outcome of playing one of a puzzle's test cases against the server (TestSession/play)--see CgPuzzleManager.play.

index instance-attribute

index

The test case's 1-based index (see CgTestSessionTestCase.index).

label instance-attribute

label

The test case's real label, from .meta/tests/<index>/ if it's been downloaded--a generic f"test {index}" placeholder otherwise (play() doesn't require an index to be locally downloaded; the server doesn't need that to run it).

result instance-attribute

result

The raw TestSession/play response for this test case.

CgPuzzleSetLanguageResult dataclass

CgPuzzleSetLanguageResult(language, previous_language, code, from_server)

The outcome of CgPuzzleManager.set_language().

language instance-attribute

language

The language now recorded in data/puzzle-data.json.

previous_language instance-attribute

previous_language

What it was before.

code instance-attribute

code

The new contents of data/solution.src.

from_server instance-attribute

from_server

True when code is the codingamer's real saved work for language, restored from the server; False when they had never attempted this puzzle in that language and code is just a generated placeholder. Worth surfacing--the difference is invisible in the file itself, and "your old solution is back" and "here's an empty starting point" are very different things to be told.

CgPuzzleStatus dataclass

CgPuzzleStatus(puzzle_dir, puzzle_id, puzzle_handle, title, puzzle_pretty_id, puzzle_type, difficulty, solution_language, local_dirty, progress)

A point-in-time summary of a puzzle working directory--see CgPuzzleManager.status(). Much simpler than codingame_tools.contribution_manager.CgContributionStatus--no versioning, no draft/moderation gate, no sync-state machine--matching this whole package's "much simpler than contribution_manager" design (see the module docstring).

puzzle_dir instance-attribute

puzzle_dir

The working directory this status describes.

puzzle_id instance-attribute

puzzle_id

Numeric ID of the puzzle (CgPuzzleIdentity.puzzle_id).

puzzle_handle instance-attribute

puzzle_handle

Opaque handle for the puzzle (CgPuzzleIdentity.puzzle_handle).

title instance-attribute

title

.meta/puzzle-server-data.json's cached title--informational only, may be stale (see CgPuzzleServerData.title's docstring).

puzzle_pretty_id instance-attribute

puzzle_pretty_id

.meta/puzzle-server-data.json's cached pretty ID/slug--informational only, may be stale (see CgPuzzleServerData.puzzle_pretty_id's docstring--never trusted as ground truth by this package itself either).

puzzle_type instance-attribute

puzzle_type

.meta/puzzle-server-data.json's cached contribution type (e.g. "PUZZLE_INOUT"), or None for a cache file written before this field existed (see CgPuzzleServerData. puzzle_type)--run cg puzzle repair (after deleting .meta/) to populate it.

difficulty instance-attribute

difficulty

.meta/puzzle-server-data.json's cached difficulty level (e.g. "easy"), or None for a cache file written before this field existed (see CgPuzzleServerData.difficulty)--same backfill note as puzzle_type.

solution_language instance-attribute

solution_language

data/puzzle-data.json's solution_language--the language data/solution.src is currently written in.

local_dirty instance-attribute

local_dirty

Whether data/solution.src currently differs from the server's last-submitted answer for this puzzle (bool(diff()))--None unless status(refresh=True) checked (a live TestSession/startTestSession call; there is no local cache of the server's answer to compare against, unlike codingame_tools.contribution_manager).

progress instance-attribute

progress

This codingamer's live progress/score summary for the puzzle (Puzzle/findProgressByIds-- level/validator_score/solved_count/attempt_count/xp_points/last_activity), or None unless status(refresh=True) fetched it.

CgPuzzleDirInferenceError

Bases: Exception

Raised by infer_puzzle_dir when target_file doesn't resolve into a puzzle working directory.

CgPuzzleDirNotFoundError

CgPuzzleDirNotFoundError()

Bases: Exception

Raised by resolve_puzzle_dir() (unless allow_default=True) when no puzzle working directory could be located by any discovery step. Does not indicate a bug--this is the normal outcome before a puzzle has been imported in the current directory.

Source code in codingame_tools/puzzle_manager/resolver.py
43
44
45
46
47
48
def __init__(self) -> None:
    super().__init__(
            "No puzzle working directory found (checked the current directory and "
            "\"./puzzle\" for a puzzle.json). Pass an explicit directory, set "
            f"{CG_PUZZLE_DIR_ENV_VAR}, or run `cg settings set puzzle-dir DIR`."
        )

CgPuzzleData dataclass

CgPuzzleData(solution_language, extra_data=dict())

Bases: JSONWizardX

The data/puzzle-data.json manifest: the one piece of metadata that genuinely travels with a solution submission (alongside data/solution.src itself), as opposed to read-only puzzle content (statement, stub generator--see .meta/) or server-assigned identity/cache. Safe to commit to git, same as solution.src.

solution_language instance-attribute

solution_language

The language data/solution.src is currently written in--submitted alongside the code on submit()/play(), and so genuinely part of the user-managed submission state, not read-only reference material or server-derived cache.

CgPuzzleIdentity dataclass

CgPuzzleIdentity(schema_version, puzzle_id, puzzle_handle, extra_data=dict())

Bases: JSONWizardX

The puzzle.json manifest: this working directory's stable identity, written once by import_() and never changed afterward. Deliberately the only thing this package considers safe to treat as permanent, git-trackable truth about which puzzle this is--see the module docstring, and codingame_tools.puzzle_manager.manager's, for why test_session_handle/title/puzzle_pretty_id are cache (.meta/) instead, not identity.

schema_version instance-attribute

schema_version

The on-disk format version this working directory was written in--see PUZZLE_SCHEMA_VERSION.

puzzle_id instance-attribute

puzzle_id

Numeric ID of the puzzle (CgTestSessionPuzzle.id)--the actual repair root key: the only confirmed API that can regenerate everything else from scratch, Puzzle/findProgressByIds, takes this, not puzzle_handle (no known API accepts the opaque handle as a lookup key) or puzzle_pretty_id (not trusted as stable--see CgPuzzleServerData.puzzle_pretty_id).

puzzle_handle instance-attribute

puzzle_handle

Opaque handle for the puzzle (CgTestSessionPuzzle.handle). Recorded here as part of this working directory's permanent identity even though nothing can look a puzzle up by it today--puzzle_id is what repair() actually queries with.

CgPuzzleServerData dataclass

CgPuzzleServerData(test_session_handle, title, puzzle_pretty_id, extra_data=dict(), puzzle_type=None, difficulty=None)

Bases: JSONWizardX

The .meta/puzzle-server-data.json manifest: cached, gitignored, re-derivable-from- puzzle_id server state. Rebuilt by repair() whenever missing (e.g. after a fresh clone into a different repo, or manual deletion/corruption).

test_session_handle instance-attribute

test_session_handle

This codingamer's test session handle for the puzzle (see CgPuzzleService.generate_session_from_puzzle_pretty_id). Freely cached and reused indefinitely, unlike puzzle_pretty_id/title below--confirmed (2026-07-30, per repeated identical results from generateSessionFromPuzzlePrettyId) to be a per-user singleton with affinity to the puzzle, not to whichever pretty_id happened to be used to generate it; there is no known scenario where a cached handle here would need re-verification the way a cached puzzle_pretty_id does.

title instance-attribute

title

Display title of the puzzle. Purely informational (e.g. for cg puzzle where output).

puzzle_pretty_id instance-attribute

puzzle_pretty_id

The puzzle's pretty ID/slug at the time this was last (re)written--informational only, never trusted as ground truth. Unlike test_session_handle, a pretty ID is not confirmed stable (it plausibly changes if the puzzle's title changes, and even a structurally-valid pretty ID string could in principle end up reassigned to a different puzzle over time)--so this cached copy is never fed back into an API call (e.g. generateSessionFromPuzzlePrettyId) by this package. Whenever a pretty ID is actually needed operationally (only repair() ever needs one, and only if findProgressByIds didn't already hand back a reusable test_session_handle directly), it's re-derived fresh from Puzzle/findProgressByIds(puzzle_id) and cross-checked against puzzle_id first--see CgPuzzleManager.repair.

puzzle_type class-attribute instance-attribute

puzzle_type = None

The puzzle's contribution type (e.g. "PUZZLE_INOUT"--currently the only type this package supports at all, so always that value in practice today), as of when this was last (re) written. Purely informational, same caching spirit as title/puzzle_pretty_id. None for a cache file written before this field existed--not re-backfilled automatically; run cg puzzle repair (after deleting .meta/) to populate it.

difficulty class-attribute instance-attribute

difficulty = None

The puzzle's difficulty level (CgTestSessionPuzzle.level/CgLastActivityPuzzle.level, e.g. "easy", "medium", "hard"), as of when this was last (re)written. Purely informational, same caching spirit as title/puzzle_pretty_id. None for a cache file written before this field existed--same backfill note as puzzle_type.

CgStatementBlock dataclass

CgStatementBlock(kind, text)

A single rendered block of a puzzle's HTML statement--see parse_statement_html.

kind instance-attribute

kind

One of "header" (a section title, e.g. "Goal"/"Input"/"Output"/"Constraints"/"Example"), "text" (ordinary paragraph/description text--may itself contain internal blank lines, e.g. the Goal section's multiple paragraphs), "example_input", or "example_output" (the literal, whitespace-preserved example test-case text under an "Example" section).

text instance-attribute

text

The block's rendered plain text (HTML tags stripped, entities unescaped).

find_solution_file

find_solution_file(data_dir, extension=None)

The existing solution file in data_dir, whatever extension it currently carries.

Callers generally know the language and so know the name, but not always: a working directory whose language changed out from under it, or one written by an older cg that used a fixed solution.src, still has to be found. The expected name wins when present, so a stray leftover can never shadow the real file; otherwise a lone solution.* is accepted.

Returns None if there is no solution file, or if several exist with no way to choose--the caller decides whether that's an error or a thing to repair.

Source code in codingame_tools/puzzle_manager/layout.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def find_solution_file(data_dir: Path, extension: str | None = None) -> Path | None:
    """The existing solution file in `data_dir`, whatever extension it currently carries.

       Callers generally know the language and so know the name, but not always: a working
       directory whose language changed out from under it, or one written by an older cg that used
       a fixed `solution.src`, still has to be found. The expected name wins when present, so a
       stray leftover can never shadow the real file; otherwise a lone `solution.*` is accepted.

       Returns None if there is no solution file, or if several exist with no way to choose--the
       caller decides whether that's an error or a thing to repair."""
    if extension is not None:
        expected = data_dir / solution_file_name(extension)
        if expected.is_file():
            return expected
    candidates = sorted(p for p in data_dir.glob(f"{SOLUTION_FILE_STEM}.*") if p.is_file())
    if len(candidates) == 1:
        return candidates[0]
    if not candidates:
        return None
    # Ambiguous: prefer the fallback name if it is one of them, since that is what an older cg
    # wrote and what a migration is most likely looking at.
    fallback = data_dir / solution_file_name(None)
    return fallback if fallback in candidates else None

solution_file_name

solution_file_name(extension)

solution.<ext> for a known language extension, else solution.src.

The file carries the language's real extension rather than a fixed one because every tool that reads it--language servers, debuggers, the compiler--dispatches on the extension. cg previously kept data/solution.src fixed and maintained a solution.<ext> symlink beside it, which cost a day of debugging: the debug info named one path, the editor resolved the other, and breakpoints silently failed to bind. One real file with the right name has no such gap, and needs no symlink support from the filesystem (which Windows only grants with developer mode enabled).

Source code in codingame_tools/puzzle_manager/layout.py
70
71
72
73
74
75
76
77
78
79
80
def solution_file_name(extension: str | None) -> str:
    """`solution.<ext>` for a known language extension, else `solution.src`.

       The file carries the language's real extension rather than a fixed one because every tool
       that reads it--language servers, debuggers, the compiler--dispatches on the extension. cg
       previously kept `data/solution.src` fixed and maintained a `solution.<ext>` symlink beside
       it, which cost a day of debugging: the debug info named one path, the editor resolved the
       other, and breakpoints silently failed to bind. One real file with the right name has no
       such gap, and needs no symlink support from the filesystem (which Windows only grants with
       developer mode enabled)."""
    return f"{SOLUTION_FILE_STEM}.{extension or SOLUTION_FALLBACK_EXTENSION}"

find_puzzle_dir

find_puzzle_dir(explicit=None, *, settings=None, start_dir=None)

Locate the puzzle working directory to use, following the documented discovery precedence:

1. `explicit` (typically the resolved value of a `--puzzle-dir` CLI flag), if given.
2. The `CG_PUZZLE_DIR` environment variable, if set.
3. `settings.current_puzzle_dir`--the *active* working directory, set by
   `cg puzzle import`/`create` and `cg puzzle activate`. Outranks the configured default
   below so that creating a working directory somewhere isn't silently overridden by a
   standing `puzzle_dir` preference pointing elsewhere.
4. `settings.puzzle_dir` (see `CgSettings.puzzle_dir`), if given and set.
5. `start_dir` (or the current directory, if not given), if it contains a `puzzle.json`.
6. `start_dir / "puzzle"`, if it contains a `puzzle.json`.

Steps 1-4 are taken at face value--the resolved directory need not contain a puzzle.json yet (e.g. before the first cg puzzle import). Steps 5-6 are implicit inference and are deliberately conservative: they only match if a puzzle.json is actually already there.

Returns:

  • Path | None

    The resolved puzzle directory path, or None if nothing was found at all. This function

  • Path | None

    never creates anything.

Source code in codingame_tools/puzzle_manager/resolver.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def find_puzzle_dir(
            explicit: Path | str | None = None,
            *,
            settings: CgSettings | None = None,
            start_dir: Path | str | None = None,
        ) -> Path | None:
    """Locate the puzzle working directory to use, following the documented discovery
       precedence:

        1. `explicit` (typically the resolved value of a `--puzzle-dir` CLI flag), if given.
        2. The `CG_PUZZLE_DIR` environment variable, if set.
        3. `settings.current_puzzle_dir`--the *active* working directory, set by
           `cg puzzle import`/`create` and `cg puzzle activate`. Outranks the configured default
           below so that creating a working directory somewhere isn't silently overridden by a
           standing `puzzle_dir` preference pointing elsewhere.
        4. `settings.puzzle_dir` (see `CgSettings.puzzle_dir`), if given and set.
        5. `start_dir` (or the current directory, if not given), if it contains a `puzzle.json`.
        6. `start_dir / "puzzle"`, if it contains a `puzzle.json`.

       Steps 1-4 are taken at face value--the resolved directory need not contain a `puzzle.json`
       yet (e.g. before the first `cg puzzle import`). Steps 5-6 are implicit inference and are
       deliberately conservative: they only match if a `puzzle.json` is actually already there.

    Returns:
        The resolved puzzle directory path, or None if nothing was found at all. This function
        never creates anything.
    """
    if explicit is not None:
        return Path(explicit).expanduser().resolve()
    env_value = os.environ.get(CG_PUZZLE_DIR_ENV_VAR)
    if env_value:
        return Path(env_value).expanduser().resolve()
    if settings is not None and settings.current_puzzle_dir is not None:
        return settings.current_puzzle_dir
    if settings is not None and settings.puzzle_dir is not None:
        return settings.puzzle_dir
    start = Path(start_dir).resolve() if start_dir is not None else Path.cwd()
    if (start / PUZZLE_IDENTITY_FILE_NAME).is_file():
        return start
    default_subdir = start / DEFAULT_PUZZLE_SUBDIR_NAME
    if (default_subdir / PUZZLE_IDENTITY_FILE_NAME).is_file():
        return default_subdir
    return None

infer_puzzle_dir

infer_puzzle_dir(target_file)

Infer a puzzle working directory's root from a solution file somewhere within it--e.g. VS Code's ${file} macro, however many symlink hops away from data/solution.src it might be (a puzzle working directory's own solution.<ext> convenience symlink, or some other symlink elsewhere entirely that a user set up themselves--see codingame_tools. puzzle_manager.manager's module docstring). The only two things ever promised about target_file: a debugger's breakpoints bind to whatever path was actually open in the editor (so this function must not need that path to be anything in particular), and resolving every symlink in it always eventually lands on data/solution.src.

So this isn't a search: fully resolving target_file (following every symlink to its real target) always lands on <root>/data/solution.src--DATA_SUBDIR_NAME/SOLUTION_FILE_NAME are fixed constants, not configurable--so <root> is deterministically two path segments up from there. Confirmed by requiring puzzle.json to actually exist at that root, so a target_file that isn't part of any puzzle working directory at all fails clearly rather than returning a nonsense path.

Raises:

  • CgPuzzleDirInferenceError

    if target_file, once fully resolved, isn't .../data/solution.src, or puzzle.json isn't present at the inferred root.

Source code in codingame_tools/puzzle_manager/resolver.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def infer_puzzle_dir(target_file: Path | str) -> Path:
    """Infer a puzzle working directory's root from a solution file somewhere within it--e.g. VS
       Code's `${file}` macro, however many symlink hops away from `data/solution.src` it might
       be (a puzzle working directory's own `solution.<ext>` convenience symlink, or some other
       symlink elsewhere entirely that a user set up themselves--see `codingame_tools.
       puzzle_manager.manager`'s module docstring). The only two things ever promised about
       `target_file`: a debugger's breakpoints bind to whatever path was actually open in the
       editor (so this function must not need that path to be anything in particular), and
       resolving every symlink in it always eventually lands on `data/solution.src`.

       So this isn't a search: fully resolving `target_file` (following every symlink to its real
       target) always lands on `<root>/data/solution.src`--`DATA_SUBDIR_NAME`/`SOLUTION_FILE_NAME`
       are fixed constants, not configurable--so `<root>` is deterministically two path segments
       up from there. Confirmed by requiring `puzzle.json` to actually exist at that root, so a
       `target_file` that isn't part of any puzzle working directory at all fails clearly rather
       than returning a nonsense path.

    Raises:
        CgPuzzleDirInferenceError: if `target_file`, once fully resolved, isn't
                                    `.../data/solution.src`, or `puzzle.json` isn't present at the
                                    inferred root.
    """
    resolved = Path(target_file).resolve()
    # Matched on the stem, not the full name: the solution file carries its language's extension
    # and is renamed when the language changes, so `solution.cpp` and `solution.py` are equally
    # valid here and the set of legal names is open-ended.
    if resolved.stem != SOLUTION_FILE_STEM or resolved.parent.name != DATA_SUBDIR_NAME:
        raise CgPuzzleDirInferenceError(
                f"{target_file} does not resolve to a {DATA_SUBDIR_NAME}/{SOLUTION_FILE_STEM}.* "
                "file--not part of a puzzle working directory."
            )
    root = resolved.parent.parent
    if not (root / PUZZLE_IDENTITY_FILE_NAME).is_file():
        raise CgPuzzleDirInferenceError(
                f"{root} has no {PUZZLE_IDENTITY_FILE_NAME}--not a puzzle working directory.")
    return root

resolve_puzzle_dir

resolve_puzzle_dir(explicit=None, *, settings=None, start_dir=None, allow_default=False)

Locate the puzzle working directory, following the discovery precedence documented on find_puzzle_dir.

If allow_default is True and no directory can be found, falls back to start_dir / "puzzle" (or ./puzzle under the current directory)--useful for cg puzzle import, which is happy to treat "nothing found" as "start a fresh working directory there". Deliberately not bare start_dir/cwd itself--unlike a contribution working directory (whose own import always requires an explicit target directory, so its resolver's allow_default fallback is never actually exercised in practice), cg puzzle import relies on this fallback for its everyday no-argument usage, and dropping puzzle.json/data/ directly into whatever the current directory happens to be would be a real footgun--confirmed live (2026-07-30): an earlier version of this fell back to bare cwd and did exactly that. submit()-style callers, where there must already be a working directory, should leave allow_default False.

Raises:

Source code in codingame_tools/puzzle_manager/resolver.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def resolve_puzzle_dir(
            explicit: Path | str | None = None,
            *,
            settings: CgSettings | None = None,
            start_dir: Path | str | None = None,
            allow_default: bool = False,
        ) -> Path:
    """Locate the puzzle working directory, following the discovery precedence documented on
       `find_puzzle_dir`.

       If `allow_default` is True and no directory can be found, falls back to
       `start_dir / "puzzle"` (or `./puzzle` under the current directory)--useful for `cg puzzle
       import`, which is happy to treat "nothing found" as "start a fresh working directory
       there". Deliberately *not* bare `start_dir`/cwd itself--unlike a contribution working
       directory (whose own `import` always requires an explicit target directory, so its
       resolver's `allow_default` fallback is never actually exercised in practice), `cg puzzle
       import` relies on this fallback for its everyday no-argument usage, and dropping
       `puzzle.json`/`data/` directly into whatever the current directory happens to be would be
       a real footgun--confirmed live (2026-07-30): an earlier version of this fell back to bare
       cwd and did exactly that. `submit()`-style callers, where there must already be a working
       directory, should leave `allow_default` False.

    Raises:
        CgPuzzleDirNotFoundError: if no directory could be located anywhere, and `allow_default`
                                   is False.
    """
    found = find_puzzle_dir(explicit, settings=settings, start_dir=start_dir)
    if found is not None:
        return found
    if allow_default:
        start = Path(start_dir).resolve() if start_dir is not None else Path.cwd()
        return start / DEFAULT_PUZZLE_SUBDIR_NAME
    raise CgPuzzleDirNotFoundError()

parse_statement_html

parse_statement_html(html)

Parse a puzzle's cached HTML statement into an ordered list of display blocks--see CgStatementBlock and the module docstring.

Source code in codingame_tools/puzzle_manager/statement_render.py
169
170
171
172
173
174
175
def parse_statement_html(html: str) -> list[CgStatementBlock]:
    """Parse a puzzle's cached HTML statement into an ordered list of display blocks--see
       `CgStatementBlock` and the module docstring."""
    parser = _StatementHTMLParser()
    parser.feed(html)
    parser.close()
    return parser.blocks