본문으로 건너뛰기
cccskills
무료GitHub에 공개

testable-game-logic

Use when starting any game task, before writing tests - find out which editors and runners exist on this machine, keep gameplay rules in engine-independent classes with an injected clock and seeded RNG, and report exactly what could not run.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구Unity
설치 방법 보기

포함된 파일(1)

  • SKILL.md8.0 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Testable Game Logic

Overview

You run headless on the user's machine. A web game or a Bevy crate can always be built and tested; a Unity, Unreal or Godot editor may or may not be installed, licensed and at the right version. The answer is the same in every engine: gameplay rules live in plain classes that a plain runner can drive, and engine callbacks are thin adapters that feed them input, time and randomness.

Core principle: If a rule can only be tested by pressing Play, it is in the wrong class. And a test you wrote but could not run is reported as exactly that — never as a pass.

What can run here

Run these first, in one step, and keep the answers for the plan.

# Unity: the exact editor version the project pins
grep m_EditorVersion ProjectSettings/ProjectVersion.txt
ls "/Applications/Unity/Hub/Editor/" 2>/dev/null            # macOS
ls "$HOME/Unity/Hub/Editor/" 2>/dev/null                    # Linux
echo "$UNITY_PATH"
# macOS binary: /Applications/Unity/Hub/Editor/<ver>/Unity.app/Contents/MacOS/Unity
# Windows: C:\Program Files\Unity\Hub\Editor\<ver>\Editor\Unity.exe ; Linux: ~/Unity/Hub/Editor/<ver>/Editor/Unity

# Godot: binary and version must match config/features (major.minor); .NET projects need the .NET build
which godot godot4 2>/dev/null; godot --version 2>/dev/null
ls /Applications/Godot*.app/Contents/MacOS/ 2>/dev/null

# Unreal: EngineAssociation in the .uproject names the install
grep EngineAssociation *.uproject
ls "/Users/Shared/Epic Games/" 2>/dev/null                  # macOS; Windows: C:\Program Files\Epic Games\UE_5.x
# binary: <engine>/Engine/Binaries/<Mac|Win64|Linux>/UnrealEditor-Cmd

node -v; cargo --version; dotnet --version; python3 --version 2>/dev/null

Rules:

  • Use only the editor whose version matches the pin. A different Unity or Unreal version re-imports and re-serializes the project; a different Godot minor rewrites project.godot.
  • Never install or download an editor on your own initiative. Missing is a finding to report, not a problem to solve.
  • Unity batchmode needs an activated licence; a licensing error in the log is "could not run", not a test failure of yours.
  • First builds are slow (Unreal C++, Bevy, Unity import). Give run_terminal a long timeout, or run detached to /tmp/tt-<task key>/<step>.log and read the log.

The layering

Rules (plain C# / GDScript RefCounted / C++ struct / TS module / Rust fn)   ← unit tests, any runner
   ↑ called by
Engine adapter (MonoBehaviour / Node / Actor component / Phaser scene / Bevy system)  ← engine tests
   ↑ wired in
Scenes, prefabs, Blueprints, data assets                                     ← looked at (game-visual-self-review)

The adapter reads input and deltaTime, calls the rule, and applies the result to the engine (transform, animation, UI event). It contains no if about game rules.

Worked example (Unity, C#)

// ❌ rule welded to the engine: untestable without Play mode, reads global time
public class Dash : MonoBehaviour {
    float _lastDash;
    void Update() {
        if (Input.GetKeyDown(KeyCode.Space) && Time.time - _lastDash > 1.5f) {
            _lastDash = Time.time;
            GetComponent<Rigidbody>().AddForce(transform.forward * 20, ForceMode.Impulse);
        }
    }
}

// ✅ rule in a plain class (assembly with noEngineReferences), time passed in
public sealed class Cooldown {
    readonly float _duration; float _remaining;
    public Cooldown(float duration) => _duration = duration;
    public bool IsReady => _remaining <= 0f;
    public void Tick(float dt) => _remaining = MathF.Max(0f, _remaining - dt);
    public bool TryConsume() { if (!IsReady) return false; _remaining = _duration; return true; }
}

[Test] public void TryConsume_WhileCoolingDown_ReturnsFalse() {
    var c = new Cooldown(1.5f);
    Assert.That(c.TryConsume(), Is.True);
    c.Tick(1.0f);
    Assert.That(c.TryConsume(), Is.False);
    c.Tick(0.5f);
    Assert.That(c.TryConsume(), Is.True);
}

The adapter DashAbility : MonoBehaviour caches its Rigidbody in Awake, ticks the cooldown in Update with Time.deltaTime, and applies the impulse in FixedUpdate when an input flag is set — three lines with no rule in them.

When the editor is missing

EngineRuns without the editorHow
UnityRules in an asmdef with "noEngineReferences": trueA throwaway net8.0 NUnit project in /tmp/tt-<task key>/ that compiles those sources: <Compile Include="<repo>/Assets/Scripts/Core/**/*.cs" />, then dotnet test. Commit the EditMode test in the repo either way.
Godot (C#)Plain C# classes with no Godot namespaceA class-library project referenced by an xUnit/NUnit project, dotnet test
Godot (GDScript)Nothing — GDScript needs the Godot binaryWrite the gdUnit4/GUT test anyway and report it as not run
UnrealNothing — even pure C++ in a module needs the engine buildWrite the Automation Spec anyway and report it as not run
Web, Bevy, MonoGame, pygameEverythingVitest, cargo test, dotnet test, pytest

If the repository already has such a plain-runner project (a Tests.Core.csproj, a logic/ crate), use it instead of a throwaway one.

Time, randomness and input are parameters

  • Time: rules take dt (or an IClock), never read Time.time, OS.get_ticks_msec(), performance.now() or Instant::now() themselves.
  • Randomness: one seeded generator passed in — new System.Random(seed), RandomNumberGenerator with .seed, a mulberry32(seed) in TS, ChaCha8Rng::seed_from_u64(seed) in Rust. A test fixes the seed; the game seeds from the clock or the save.
  • Input: rules receive intents (MoveIntent(x, y), Jump), not key codes. Rebinding then never touches a rule.

Tests that games specifically need

  • Two-delta test for anything time-based: run 1 s as 60×(1/60) and as 30×(1/30); assert the same result within tolerance (frame-rate-independence).
  • Determinism test where replays, lockstep or rollback exist: same seed + same input log → same state hash after N steps.
  • Table-driven transition test for every state machine (game-state-machines-and-ai).
  • Data invariant test over every shipped data asset: no zero-weight drop table, no negative damage, every referenced ID exists (data-driven-gameplay-design).
  • Regression test for every bug, watched failing before the fix.
  • Performance test with a threshold that fails when exceeded, for a hot path you changed (game-performance-budget).

Naming follows the repository; without a convention: NUnit Method_Scenario_Expected, gdUnit4/GUT test_<scenario>_<expected>, Unreal Project.System.Scenario, Vitest/cargo test a sentence describing the behaviour.

Reporting what could not run

One line per command in the closing message, verbatim:

NOT RUN: Unity 6000.3.2f1 not installed (Hub has 6000.0.40f1 only) — EditMode tests in Assets/Tests/EditMode/CooldownTests.cs written, not executed.
RAN: dotnet test /tmp/tt-T-42/core-tests → 12 passed, 0 failed (Core sources compiled without UnityEngine).

Common Mistakes

  • Testing a rule through a MonoBehaviour, Node or Actor because "that's where the code was".
  • Reading global time or Random inside a rule, then fighting flaky tests.
  • Running the tests in a mismatched editor version and committing the re-import churn.
  • Calling a run "verified" when the headless editor exited on a licence error.

Red Flags

  • A rule class that imports UnityEngine, Godot, Phaser or bevy::prelude without needing it.
  • A closing message with no RAN:/NOT RUN: accounting on an engine project.
  • A test that only passes at one frame rate.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

makifbaysal의 스킬 모두 보기

이 스킬의 문제 신고