Skip to content

codingame_tools.puzzle_manager.manager

manager

CgPuzzleManager: builds a puzzle working directory from an existing server-side puzzle (import_), runs the working directory's current solution against a single test case (play), submits it for credit (submit), and reconstructs cached/reference state that was deliberately never committed to git (repair).

Deliberately much simpler than codingame_tools.contribution_manager: exactly one file is ever editable--data/solution.src--so there is no git repository backing this working directory, no branches, no multi-file merge machinery. "Merge reconciliation" here is just a two-way choice between the local file and the server's last-submitted version:

  • diff() shows a unified text diff between them.
  • discard_local() overwrites the local file with the server's version.
  • submit() overwrites the server's version with the local file (a normal TestSession/submit). Note play() also durably updates the server's copy of the code as a side effect (see its docstring)--unlike a contribution, a puzzle working directory has two independent server-side persistence phases (the test session's current answer, and a graded submission), not one; submit() is named for CodinGame's own vocabulary (matching the underlying TestSession/submit API method) rather than push()'s git vocabulary, precisely to avoid implying it's the only thing that persists anything server-side.

There is no third "merge tool" option in this first cut--flagged as a possible follow-up, not built, since a single-file external diff/merge tool is easy to add later if actually wanted.

Unlike a contribution, nothing here is ever newly created: a puzzle already exists on the server before you can solve it, so import_() is the only way a working directory comes into being, and CgPuzzleIdentity has no create()-then-later-linked state to track.

Three-way state split (see codingame_tools.puzzle_manager.schema/.layout for the exact files), and why: a puzzle working directory is expected to be put under the user's own git (unlike a contribution working directory, which has its own, separate, internal git repo). That means anything not explicitly committed is lost the moment the directory is cloned into a different repo/machine--so state here is split by how it behaves under that constraint:

  • puzzle.json (CgPuzzleIdentity, root): the only facts treated as permanent identity, safe to commit--puzzle_id and puzzle_handle. Deliberately minimal: puzzle_id is the real repair root key (the only confirmed API that can regenerate everything else, Puzzle/findProgressByIds, takes a numeric ID, not a pretty ID or the opaque handle).
  • .meta/ (CgPuzzleServerData + read-only statement.html/stub_generator.cgstub/tests/): gitignored cache, reconstructed by repair() whenever missing. test_session_handle is cached and reused freely (confirmed stable, with affinity to the puzzle, not to whichever pretty ID happened to generate it). title/puzzle_pretty_id are cached too, but purely for display--never trusted as ground truth or fed back into an API call, since (unlike the handle) a pretty ID isn't confirmed stable across e.g. a puzzle title change. See CgPuzzleServerData's own docstring. tests/ (see codingame_tools.puzzle_manager.test_cases_dir) holds each test case's downloaded input/output, one directory per server-assigned test index--reference material for running the solution locally (e.g. in a debugger), not something this package interprets itself.
  • data/puzzle-data.json (CgPuzzleData) + data/solution.src: genuinely user-managed, git-trackable content--the solution itself, and the one piece of metadata that travels with a submission (solution_language).

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.

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).

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).

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.

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.

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.

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}")

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}")

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.

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
        )

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.

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.

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)

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}"