carthagent 0.0.0-stage → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -2
- package/bin/ctg.mjs +736 -0
- package/docs/budgets.md +72 -0
- package/docs/compaction.md +34 -0
- package/docs/context-efficiency-plan.md +24 -0
- package/docs/context-efficiency-review.md +27 -0
- package/docs/context-efficiency.md +49 -0
- package/docs/deepseek-jev-benchmark-plan.md +70 -0
- package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
- package/docs/deepseek-jev-benchmark-restart.md +9 -0
- package/docs/deepseek-jev-benchmark-results.md +77 -0
- package/docs/deepseek-jev-benchmark-running.md +60 -0
- package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
- package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
- package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
- package/docs/deepseek-jev-http400-diagnosis.md +45 -0
- package/docs/delivery-review-evidence.md +53 -0
- package/docs/delivery-timeout-repair-plan.md +51 -0
- package/docs/evaluation-readiness.md +31 -0
- package/docs/evaluation.md +70 -0
- package/docs/extending.md +354 -0
- package/docs/glm-pilot-execution.md +9 -0
- package/docs/glm-pilot-plan.md +15 -0
- package/docs/glm-pilot-results.md +71 -0
- package/docs/graph-context-optimization.md +15 -0
- package/docs/jev-repository-pilot-plan.md +25 -0
- package/docs/jev-repository-pilot-protocol.md +39 -0
- package/docs/jev-repository-pilot-restart.md +7 -0
- package/docs/jev-repository-pilot-results.md +79 -0
- package/docs/jev.md +32 -0
- package/docs/model-registry.md +25 -0
- package/docs/native-compatibility.md +117 -0
- package/docs/opencode-archive.md +9 -0
- package/docs/opencode-migration.md +115 -0
- package/docs/opencode-runtime.md +94 -0
- package/docs/opencode-workspace.md +31 -0
- package/docs/owned-runtime-workflow-registration.md +17 -0
- package/docs/plan-advisor-evaluation.md +38 -0
- package/docs/planning-startup-fix-plan.md +23 -0
- package/docs/planning-startup-fix-review.md +24 -0
- package/docs/planning.md +118 -0
- package/docs/production-merge-review.md +23 -0
- package/docs/quality.md +35 -0
- package/docs/repository-benchmark-design.md +111 -0
- package/docs/review-assurance.md +9 -0
- package/docs/review-experiment-results.md +59 -0
- package/docs/tested-planning-roadmap.md +58 -0
- package/docs/tested-planning.md +58 -0
- package/docs/tui-pickers.md +9 -0
- package/docs/webdriver.md +83 -0
- package/docs/workflow-modes.md +26 -0
- package/docs/workflow.d2 +17 -0
- package/docs/workflow.svg +149 -0
- package/docs/workspace-coordination.md +9 -0
- package/evaluate.mjs +249 -0
- package/extensions/codegraph.ts +36 -0
- package/extensions/delivery.ts +819 -0
- package/extensions/experiential.ts +21 -0
- package/extensions/subscriptions.ts +21 -0
- package/guidance/profiles/authenticated-web.json +24 -0
- package/guidance/profiles/data-visualization.json +25 -0
- package/guidance/profiles/database-change.json +24 -0
- package/guidance/profiles/external-api.json +24 -0
- package/guidance/profiles/transactional-data.json +25 -0
- package/guidance/profiles/web-application.json +26 -0
- package/lib/acceptance-review.mjs +4 -0
- package/lib/browser-bin.mjs +160 -0
- package/lib/browser-errors.mjs +54 -0
- package/lib/browser.mjs +219 -0
- package/lib/budget.mjs +37 -0
- package/lib/cdp.mjs +221 -0
- package/lib/cloud/client.mjs +275 -0
- package/lib/cloud/control-plane.mjs +350 -0
- package/lib/cloud/gateway.mjs +380 -0
- package/lib/cloud/http.mjs +135 -0
- package/lib/cloud/ledger.mjs +215 -0
- package/lib/codegraph-query.mjs +60 -0
- package/lib/codegraph.mjs +378 -0
- package/lib/context-archive.mjs +18 -0
- package/lib/delivery-context.mjs +132 -0
- package/lib/delivery-guidance.mjs +9 -0
- package/lib/delivery-review.mjs +94 -0
- package/lib/delivery.mjs +610 -0
- package/lib/engine.mjs +137 -0
- package/lib/guidance.mjs +71 -0
- package/lib/jev-advisor.mjs +47 -0
- package/lib/opencode/archive.mjs +122 -0
- package/lib/opencode/compatibility.mjs +19 -0
- package/lib/opencode/credentials.mjs +205 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
- package/lib/opencode/evaluation.mjs +242 -0
- package/lib/opencode/file-lock.mjs +86 -0
- package/lib/opencode/headless.mjs +200 -0
- package/lib/opencode/review.mjs +213 -0
- package/lib/opencode/runtime.mjs +246 -0
- package/lib/opencode/service.mjs +249 -0
- package/lib/opencode/session-options.mjs +60 -0
- package/lib/opencode/workspace.mjs +133 -0
- package/lib/openrouter-jev.mjs +37 -0
- package/lib/outcome-contract.mjs +21 -0
- package/lib/plan-advisor.mjs +38 -0
- package/lib/planning-access.mjs +7 -0
- package/lib/planning-advice.mjs +28 -0
- package/lib/planning-discovery.mjs +14 -0
- package/lib/planning-guide.mjs +34 -0
- package/lib/planning-view.mjs +57 -0
- package/lib/planning.mjs +155 -0
- package/lib/probe-diagnostics.mjs +13 -0
- package/lib/providers/experiential.mjs +292 -0
- package/lib/providers/model-catalog.json +700 -0
- package/lib/providers/model-catalog.mjs +7 -0
- package/lib/providers/names.mjs +13 -0
- package/lib/providers/subscription-models.mjs +483 -0
- package/lib/reports.mjs +80 -0
- package/lib/review-state.mjs +49 -0
- package/lib/review.mjs +114 -0
- package/lib/scenarios.mjs +99 -0
- package/lib/ship-auth.mjs +44 -0
- package/lib/ship-jev.mjs +59 -0
- package/lib/ship-telemetry.mjs +183 -0
- package/lib/tui/ansi.mjs +255 -0
- package/lib/tui/app.mjs +2445 -0
- package/lib/tui/auth.mjs +249 -0
- package/lib/tui/carthage.mjs +109 -0
- package/lib/tui/clipboard.mjs +122 -0
- package/lib/tui/d2.mjs +431 -0
- package/lib/tui/feed.mjs +467 -0
- package/lib/tui/framing.mjs +80 -0
- package/lib/tui/glyphs.mjs +172 -0
- package/lib/tui/hash-worker.mjs +11 -0
- package/lib/tui/mock-engine.mjs +222 -0
- package/lib/tui/models.mjs +73 -0
- package/lib/tui/picker.mjs +18 -0
- package/lib/tui/theme.mjs +663 -0
- package/lib/tui/title.mjs +7 -0
- package/lib/update-channel.mjs +62 -0
- package/lib/update.mjs +230 -0
- package/lib/webdriver.mjs +82 -0
- package/lib/workflow.mjs +88 -0
- package/lib/workspace.mjs +39 -0
- package/metadata.json +8 -0
- package/package.json +94 -4
- package/plugins/carthagent/index.ts +3 -0
- package/plugins/carthagent/package.json +28 -0
- package/plugins/carthagent/src/archive-rpc.ts +46 -0
- package/plugins/carthagent/src/bootstrap.ts +38 -0
- package/plugins/carthagent/src/index.ts +62 -0
- package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
- package/plugins/carthagent/src/rpc.ts +76 -0
- package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
- package/plugins/carthagent/src/server/budgets.ts +21 -0
- package/plugins/carthagent/src/server/codegraph.ts +46 -0
- package/plugins/carthagent/src/server/delivery.ts +488 -0
- package/plugins/carthagent/src/server/device-auth.ts +91 -0
- package/plugins/carthagent/src/server/native-delivery.ts +26 -0
- package/plugins/carthagent/src/server/orchestration.ts +56 -0
- package/plugins/carthagent/src/server/policy.ts +24 -0
- package/plugins/carthagent/src/server/providers.ts +424 -0
- package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
- package/plugins/carthagent/src/server/session-controls.ts +82 -0
- package/plugins/carthagent/src/server/telemetry.ts +92 -0
- package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
- package/plugins/carthagent/src/session-rpc.ts +31 -0
- package/plugins/carthagent/src/testing.ts +85 -0
- package/plugins/carthagent/src/workspace-rpc.ts +133 -0
- package/plugins/carthagent/test-plugin/index.ts +4 -0
- package/plugins/carthagent/test-plugin/package.json +1 -0
- package/plugins/carthagent/tsconfig.json +19 -0
- package/plugins/carthagent/tui.ts +1 -0
- package/prompts/delivery.md +7 -0
- package/public/index.html +983 -0
- package/scenarios/cli/grade_cli.py +69 -0
- package/scenarios/cli/scenario.json +21 -0
- package/scenarios/game/baseline/minecraft.py +193 -0
- package/scenarios/game/context.md +1 -0
- package/scenarios/game/grade_minecraft_clone.py +603 -0
- package/scenarios/game/requirements.txt +2 -0
- package/scenarios/game/scenario.json +33 -0
- package/server.mjs +341 -0
- package/skills/game-development/SKILL.md +36 -0
- package/skills/game-development/assets/ursina_starter.py +98 -0
- package/skills/game-development/scripts/verify_ursina.py +80 -0
- package/skills/software-delivery/SKILL.md +50 -0
- package/skills/typescript-delivery/SKILL.md +69 -0
- package/tests/acceptance-review.test.mjs +13 -0
- package/tests/auth.test.mjs +234 -0
- package/tests/bounded-summary.test.mjs +68 -0
- package/tests/browser-bin.test.mjs +91 -0
- package/tests/browser.test.mjs +96 -0
- package/tests/budget-engine.test.mjs +110 -0
- package/tests/budget.test.mjs +23 -0
- package/tests/cli.test.mjs +245 -0
- package/tests/clipboard.test.mjs +79 -0
- package/tests/cloud-client.test.mjs +129 -0
- package/tests/cloud-control-plane.test.mjs +144 -0
- package/tests/cloud-gateway.test.mjs +147 -0
- package/tests/cloud-http.test.mjs +102 -0
- package/tests/cloud-ledger.test.mjs +75 -0
- package/tests/codegraph-query.test.mjs +66 -0
- package/tests/codegraph.test.mjs +142 -0
- package/tests/delivery-context.test.mjs +66 -0
- package/tests/delivery-review.test.mjs +139 -0
- package/tests/delivery.test.mjs +343 -0
- package/tests/engine-api.mjs +24 -0
- package/tests/experiential-extension.test.mjs +37 -0
- package/tests/experiential.test.mjs +300 -0
- package/tests/extension.test.mjs +921 -0
- package/tests/fixtures/archive-plugin/index.ts +10 -0
- package/tests/guidance.test.mjs +78 -0
- package/tests/model-registry.test.mjs +68 -0
- package/tests/models.test.mjs +70 -0
- package/tests/openai-idempotency.test.mjs +99 -0
- package/tests/opencode-archive-rpc.test.mjs +82 -0
- package/tests/opencode-delivery.test.mjs +231 -0
- package/tests/opencode-providers.test.mjs +414 -0
- package/tests/opencode-runtime.test.mjs +455 -0
- package/tests/opencode-session-controls.test.mjs +115 -0
- package/tests/opencode-session-options.test.mjs +26 -0
- package/tests/opencode-workspace.test.mjs +339 -0
- package/tests/outcome-contract.test.mjs +17 -0
- package/tests/picker.test.mjs +34 -0
- package/tests/plan-advisor.test.mjs +88 -0
- package/tests/plan-review-loop.test.mjs +53 -0
- package/tests/planning-advice.test.mjs +27 -0
- package/tests/planning-contract.test.mjs +65 -0
- package/tests/planning-startup.test.mjs +38 -0
- package/tests/planning-view.test.mjs +33 -0
- package/tests/planning.test.mjs +223 -0
- package/tests/probe-diagnostics.test.mjs +21 -0
- package/tests/pull-request-rpc.test.mjs +366 -0
- package/tests/review-state.test.mjs +32 -0
- package/tests/review.test.mjs +127 -0
- package/tests/server.test.mjs +252 -0
- package/tests/ship-jev.test.mjs +77 -0
- package/tests/ship-telemetry.test.mjs +333 -0
- package/tests/standard-workflow.test.mjs +28 -0
- package/tests/subscription-models.test.mjs +416 -0
- package/tests/subscriptions-extension.test.mjs +27 -0
- package/tests/terminal-title.test.mjs +13 -0
- package/tests/test_grade_minecraft_clone.py +199 -0
- package/tests/test_starter.py +78 -0
- package/tests/theme.test.mjs +175 -0
- package/tests/tui-copy-paste.test.mjs +54 -0
- package/tests/tui.test.mjs +808 -0
- package/tests/update-channels.test.mjs +129 -0
- package/tests/update.test.mjs +175 -0
- package/tests/webdriver.test.mjs +31 -0
- package/tests/workflow.test.mjs +47 -0
- package/tests/workspace-rpc.test.mjs +236 -0
- package/tests/workspace.test.mjs +24 -0
- package/tools/benchmark/advisor.test.mjs +7 -0
- package/tools/benchmark/advisor.ts +27 -0
- package/tools/benchmark/campaign.mjs +45 -0
- package/tools/benchmark/diagnose-request.mjs +40 -0
- package/tools/benchmark/eval-common.mjs +32 -0
- package/tools/benchmark/eval-ledger.mjs +22 -0
- package/tools/benchmark/eval-shop.mjs +26 -0
- package/tools/benchmark/eval-voxel.mjs +20 -0
- package/tools/benchmark/evaluate.mjs +24 -0
- package/tools/benchmark/evaluators.test.mjs +13 -0
- package/tools/benchmark/fixtures/shop/server.mjs +18 -0
- package/tools/benchmark/fixtures/shop/ui.html +8 -0
- package/tools/benchmark/freeze.mjs +30 -0
- package/tools/benchmark/gateway.mjs +91 -0
- package/tools/benchmark/gateway.test.mjs +38 -0
- package/tools/benchmark/generator.mjs +11 -0
- package/tools/benchmark/generator.test.mjs +26 -0
- package/tools/benchmark/guard.test.mjs +133 -0
- package/tools/benchmark/guard.ts +136 -0
- package/tools/benchmark/native-plugin/index.ts +24 -0
- package/tools/benchmark/planning-startup.test.mjs +47 -0
- package/tools/benchmark/preflight-live.mjs +31 -0
- package/tools/benchmark/preflight.mjs +22 -0
- package/tools/benchmark/replay-diagnostic.mjs +23 -0
- package/tools/benchmark/runner.mjs +141 -0
- package/tools/benchmark/runner.test.mjs +93 -0
- package/tools/benchmark/runtime.mjs +23 -0
- package/tools/benchmark/selftest.mjs +47 -0
- package/tools/benchmark/smoke-jev.mjs +5 -0
- package/tools/benchmark/tasks.mjs +20 -0
- package/tools/benchmark/transport.mjs +65 -0
- package/tools/benchmark/transport.test.mjs +44 -0
- package/tools/benchmark/upstream-error.mjs +18 -0
- package/tools/benchmark/upstream-error.test.mjs +10 -0
- package/tools/browser-check.mjs +109 -0
- package/tools/check-syntax.mjs +27 -0
- package/tools/evaluate-plan-advisor.mjs +20 -0
- package/tools/native-fixture.mjs +194 -0
- package/tools/native-pty-smoke.py +108 -0
- package/tools/native-report-plugin/index.ts +50 -0
- package/tools/native-report-plugin/package.json +7 -0
- package/tools/patch-compaction.mjs +24 -0
- package/tools/patch-model-catalog.mjs +12 -0
- package/tools/patch-print-shutdown.mjs +39 -0
- package/tools/repo-pilot/evaluator.cjs +32 -0
- package/tools/repo-pilot/reference.mjs +43 -0
- package/tools/repo-pilot/runner.mjs +85 -0
- package/tools/repo-pilot/sandbox-entry.mjs +12 -0
- package/tools/repo-pilot/tasks.mjs +20 -0
- package/tools/repo-pilot/type-evaluator.cjs +6 -0
- package/tools/test-native-controls.mjs +84 -0
- package/tools/test-native-delivery-host.mjs +307 -0
- package/tools/test-native-evaluation.mjs +46 -0
- package/tools/test-native-host.mjs +385 -0
- package/tools/test-native-outcomes.mjs +73 -0
- package/tools/test-native-package.mjs +70 -0
- package/tools/test-native-pty.mjs +20 -0
- package/tools/test-native-report.mjs +88 -0
- package/tools/test-native-swe-host.mjs +132 -0
- package/tools/test-native-swe.mjs +122 -0
- package/tools/test-picker-pty.py +45 -0
- package/tools/test-python.mjs +29 -0
- package/tools/test-webdriver.mjs +54 -0
- package/tools/verify-release-version.mjs +37 -0
- package/tools/webdriver-check.mjs +52 -0
- package/vendor/agent/bounded-summary.mjs +88 -0
- package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
- package/vendor/agent/chunks/anthropic.js +75 -0
- package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
- package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
- package/vendor/agent/chunks/chord-context.js +96 -0
- package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
- package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
- package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
- package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
- package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
- package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
- package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
- package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
- package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
- package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
- package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
- package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
- package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
- package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
- package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
- package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
- package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
- package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
- package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
- package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
- package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
- package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
- package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
- package/vendor/agent/chunks/github-copilot.js +2 -0
- package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
- package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
- package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
- package/vendor/agent/chunks/image-resize-worker.js +2 -0
- package/vendor/agent/chunks/kimi-coding.js +2 -0
- package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
- package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
- package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
- package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
- package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
- package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
- package/vendor/agent/chunks/openai-codex.js +75 -0
- package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
- package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
- package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
- package/vendor/agent/chunks/openrouter.js +75 -0
- package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
- package/vendor/agent/chunks/radius.js +75 -0
- package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
- package/vendor/agent/chunks/xai.js +2 -0
- package/vendor/agent/cli.js +3 -0
- package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/index.js +2 -0
- package/vendor/agent/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/package.json +14 -0
- package/vendor/agent/rpc-entry.js +3 -0
- package/vendor/agent/theme/dark.json +91 -0
- package/vendor/agent/theme/light.json +90 -0
- package/vendor/agent/theme/theme-schema.json +356 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""External acceptance grader for generated Minecraft-style Ursina games.
|
|
3
|
+
|
|
4
|
+
This is a scenario-level quality gate, not core harness logic. It checks that
|
|
5
|
+
the submitted project proves the user-visible behaviors it claims: docs/tests,
|
|
6
|
+
safe import, real renderer startup, structured smoke evidence, persisted state,
|
|
7
|
+
and an independent normal-window keyboard probe.
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import ast
|
|
11
|
+
import importlib.util
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from PIL import Image, ImageChops
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
MARKER = "GAME_SMOKE_RESULT "
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Failure(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run(argv, cwd, timeout=90):
|
|
32
|
+
try:
|
|
33
|
+
return subprocess.run(
|
|
34
|
+
argv,
|
|
35
|
+
cwd=cwd,
|
|
36
|
+
text=True,
|
|
37
|
+
stdout=subprocess.PIPE,
|
|
38
|
+
stderr=subprocess.STDOUT,
|
|
39
|
+
timeout=timeout,
|
|
40
|
+
)
|
|
41
|
+
except subprocess.TimeoutExpired as error:
|
|
42
|
+
output = error.stdout or ""
|
|
43
|
+
if isinstance(output, bytes):
|
|
44
|
+
output = output.decode(errors="replace")
|
|
45
|
+
return subprocess.CompletedProcess(
|
|
46
|
+
argv,
|
|
47
|
+
124,
|
|
48
|
+
f"{output}\nTimed out after {timeout} seconds",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fail_if(condition, message, failures):
|
|
53
|
+
if condition:
|
|
54
|
+
failures.append(message)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def import_safely(entry):
|
|
58
|
+
try:
|
|
59
|
+
from direct.showbase.ShowBase import ShowBase
|
|
60
|
+
except Exception:
|
|
61
|
+
ShowBase = None
|
|
62
|
+
opened = []
|
|
63
|
+
original_init = ShowBase.__init__ if ShowBase is not None else None
|
|
64
|
+
|
|
65
|
+
def blocked_init(self, *args, **kwargs):
|
|
66
|
+
opened.append(True)
|
|
67
|
+
raise RuntimeError("minecraft.py opened a graphics window during import")
|
|
68
|
+
|
|
69
|
+
if ShowBase is not None:
|
|
70
|
+
ShowBase.__init__ = blocked_init
|
|
71
|
+
old_path = list(sys.path)
|
|
72
|
+
try:
|
|
73
|
+
sys.path.insert(0, str(entry.parent))
|
|
74
|
+
spec = importlib.util.spec_from_file_location("candidate_minecraft", entry)
|
|
75
|
+
module = importlib.util.module_from_spec(spec)
|
|
76
|
+
spec.loader.exec_module(module)
|
|
77
|
+
finally:
|
|
78
|
+
sys.path = old_path
|
|
79
|
+
if ShowBase is not None:
|
|
80
|
+
ShowBase.__init__ = original_init
|
|
81
|
+
if opened:
|
|
82
|
+
raise Failure("Importing minecraft.py must not construct Ursina/ShowBase")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def sampled_colors_and_dark_ratio(path):
|
|
86
|
+
with Image.open(path) as image:
|
|
87
|
+
rgb = image.convert("RGB")
|
|
88
|
+
pixels = [
|
|
89
|
+
rgb.getpixel((x, y))
|
|
90
|
+
for x in range(0, rgb.width, max(1, rgb.width // 48))
|
|
91
|
+
for y in range(0, rgb.height, max(1, rgb.height // 48))
|
|
92
|
+
]
|
|
93
|
+
colors = len(set(pixels))
|
|
94
|
+
dark = sum(1 for r, g, b in pixels if r + g + b < 90) / max(1, len(pixels))
|
|
95
|
+
return colors, dark
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def changed_pixels(first, second):
|
|
99
|
+
with Image.open(first) as before, Image.open(second) as after:
|
|
100
|
+
difference = ImageChops.difference(before.convert("RGB"), after.convert("RGB"))
|
|
101
|
+
return sum(1 for pixel in difference.getdata() if pixel != (0, 0, 0))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def playable_frame(path):
|
|
105
|
+
colors, dark = sampled_colors_and_dark_ratio(path)
|
|
106
|
+
return colors >= 8 and dark <= 0.70
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def nested_ursina_callbacks(entry):
|
|
110
|
+
try:
|
|
111
|
+
tree = ast.parse(entry.read_text(errors="replace"))
|
|
112
|
+
except SyntaxError:
|
|
113
|
+
return []
|
|
114
|
+
parents = {}
|
|
115
|
+
for parent in ast.walk(tree):
|
|
116
|
+
for child in ast.iter_child_nodes(parent):
|
|
117
|
+
parents[child] = parent
|
|
118
|
+
return sorted({
|
|
119
|
+
node.name
|
|
120
|
+
for node in ast.walk(tree)
|
|
121
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
122
|
+
and node.name in {"input", "update"}
|
|
123
|
+
and isinstance(parents.get(node), (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def xdotool(*argv, timeout=5):
|
|
128
|
+
return subprocess.run(
|
|
129
|
+
["xdotool", *argv],
|
|
130
|
+
text=True,
|
|
131
|
+
stdout=subprocess.PIPE,
|
|
132
|
+
stderr=subprocess.STDOUT,
|
|
133
|
+
timeout=timeout,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def wait_for_window(process, timeout=12):
|
|
138
|
+
deadline = time.monotonic() + timeout
|
|
139
|
+
while time.monotonic() < deadline:
|
|
140
|
+
result = xdotool("search", "--onlyvisible", "--pid", str(process.pid))
|
|
141
|
+
windows = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
142
|
+
if windows:
|
|
143
|
+
return windows[-1]
|
|
144
|
+
if process.poll() is not None:
|
|
145
|
+
break
|
|
146
|
+
time.sleep(0.2)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def window_geometry(window_id):
|
|
151
|
+
result = xdotool("getwindowgeometry", "--shell", window_id)
|
|
152
|
+
values = {}
|
|
153
|
+
for line in result.stdout.splitlines():
|
|
154
|
+
if "=" in line:
|
|
155
|
+
key, value = line.split("=", 1)
|
|
156
|
+
values[key] = int(value)
|
|
157
|
+
if not {"X", "Y", "WIDTH", "HEIGHT"}.issubset(values):
|
|
158
|
+
raise Failure(f"Game window disappeared during interaction: {result.stdout.strip()}")
|
|
159
|
+
return values["X"], values["Y"], values["WIDTH"], values["HEIGHT"]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def capture_window(window_id, destination):
|
|
163
|
+
x, y, width, height = window_geometry(window_id)
|
|
164
|
+
result = subprocess.run(
|
|
165
|
+
["scrot", "-o", "-a", f"{x},{y},{width},{height}", str(destination)],
|
|
166
|
+
text=True,
|
|
167
|
+
stdout=subprocess.PIPE,
|
|
168
|
+
stderr=subprocess.STDOUT,
|
|
169
|
+
timeout=10,
|
|
170
|
+
)
|
|
171
|
+
if result.returncode != 0 or not destination.is_file():
|
|
172
|
+
raise Failure(f"Could not capture game window: {result.stdout[-1000:]}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def press(window_id, key, hold=0):
|
|
176
|
+
xdotool("windowfocus", "--sync", window_id)
|
|
177
|
+
if hold:
|
|
178
|
+
xdotool("keydown", key)
|
|
179
|
+
time.sleep(hold)
|
|
180
|
+
xdotool("keyup", key)
|
|
181
|
+
else:
|
|
182
|
+
xdotool("key", key)
|
|
183
|
+
time.sleep(0.35)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def require_running(process, log_path):
|
|
187
|
+
if process.poll() is not None:
|
|
188
|
+
raise Failure(
|
|
189
|
+
f"Game exited during interactive input (code {process.returncode}):\n"
|
|
190
|
+
f"{log_path.read_text(errors='replace')[-2000:]}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def check_interactive_window(candidate, entry, artifacts, failures):
|
|
195
|
+
if not os.environ.get("DISPLAY") or not shutil.which("xdotool") or not shutil.which("scrot"):
|
|
196
|
+
failures.append("Interactive probe needs DISPLAY, xdotool, and scrot")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
failure_count = len(failures)
|
|
200
|
+
log_path = artifacts / "grade-interactive.log"
|
|
201
|
+
save_path = artifacts / "grade-interactive-save.json"
|
|
202
|
+
save_path.unlink(missing_ok=True)
|
|
203
|
+
with log_path.open("w") as log:
|
|
204
|
+
process = subprocess.Popen(
|
|
205
|
+
[sys.executable, str(entry), "--save", str(save_path)],
|
|
206
|
+
cwd=candidate,
|
|
207
|
+
stdout=log,
|
|
208
|
+
stderr=subprocess.STDOUT,
|
|
209
|
+
text=True,
|
|
210
|
+
)
|
|
211
|
+
try:
|
|
212
|
+
try:
|
|
213
|
+
window_id = wait_for_window(process)
|
|
214
|
+
if window_id is None:
|
|
215
|
+
failures.append(f"Game did not open an interactive window:\n{log_path.read_text(errors='replace')[-2000:]}")
|
|
216
|
+
return
|
|
217
|
+
time.sleep(1)
|
|
218
|
+
require_running(process, log_path)
|
|
219
|
+
captures = {"initial": artifacts / "grade-interactive-initial.png"}
|
|
220
|
+
capture_window(window_id, captures["initial"])
|
|
221
|
+
|
|
222
|
+
press(window_id, "w", hold=0.7)
|
|
223
|
+
require_running(process, log_path)
|
|
224
|
+
captures["move"] = artifacts / "grade-interactive-move.png"
|
|
225
|
+
capture_window(window_id, captures["move"])
|
|
226
|
+
fail_if(
|
|
227
|
+
not playable_frame(captures["move"]),
|
|
228
|
+
"Interactive movement left no visible playable world",
|
|
229
|
+
failures,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
xdotool("windowfocus", "--sync", window_id)
|
|
233
|
+
xdotool("mousemove_relative", "--", "100", "0")
|
|
234
|
+
time.sleep(0.35)
|
|
235
|
+
require_running(process, log_path)
|
|
236
|
+
captures["look"] = artifacts / "grade-interactive-look.png"
|
|
237
|
+
capture_window(window_id, captures["look"])
|
|
238
|
+
fail_if(
|
|
239
|
+
changed_pixels(captures["move"], captures["look"]) < 100,
|
|
240
|
+
"Interactive mouse movement did not visibly change the camera",
|
|
241
|
+
failures,
|
|
242
|
+
)
|
|
243
|
+
fail_if(
|
|
244
|
+
not playable_frame(captures["look"]),
|
|
245
|
+
"Interactive mouselook left no visible playable world",
|
|
246
|
+
failures,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
press(window_id, "space")
|
|
250
|
+
require_running(process, log_path)
|
|
251
|
+
captures["jump"] = artifacts / "grade-interactive-jump.png"
|
|
252
|
+
capture_window(window_id, captures["jump"])
|
|
253
|
+
fail_if(
|
|
254
|
+
changed_pixels(captures["look"], captures["jump"]) < 100,
|
|
255
|
+
"Interactive Space key did not visibly trigger a jump/gravity response",
|
|
256
|
+
failures,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
press(window_id, "2")
|
|
260
|
+
require_running(process, log_path)
|
|
261
|
+
captures["select"] = artifacts / "grade-interactive-select.png"
|
|
262
|
+
capture_window(window_id, captures["select"])
|
|
263
|
+
|
|
264
|
+
press(window_id, "p")
|
|
265
|
+
require_running(process, log_path)
|
|
266
|
+
captures["pause"] = artifacts / "grade-interactive-pause.png"
|
|
267
|
+
capture_window(window_id, captures["pause"])
|
|
268
|
+
press(window_id, "p")
|
|
269
|
+
|
|
270
|
+
save_path.unlink(missing_ok=True)
|
|
271
|
+
press(window_id, "s")
|
|
272
|
+
require_running(process, log_path)
|
|
273
|
+
fail_if(
|
|
274
|
+
not save_path.is_file(),
|
|
275
|
+
"Interactive S key did not write the requested save file",
|
|
276
|
+
failures,
|
|
277
|
+
)
|
|
278
|
+
if save_path.is_file():
|
|
279
|
+
try:
|
|
280
|
+
interactive_save = json.loads(save_path.read_text())
|
|
281
|
+
except Exception as error:
|
|
282
|
+
failures.append(f"Interactive save file is not valid JSON: {error}")
|
|
283
|
+
else:
|
|
284
|
+
fail_if(
|
|
285
|
+
world_size(interactive_save) < 20,
|
|
286
|
+
"Interactive save does not contain the playable world",
|
|
287
|
+
failures,
|
|
288
|
+
)
|
|
289
|
+
fail_if(
|
|
290
|
+
selected_value(interactive_save) in (None, 0, "0", "dirt"),
|
|
291
|
+
"Interactive save did not preserve number-key selection",
|
|
292
|
+
failures,
|
|
293
|
+
)
|
|
294
|
+
selected = selected_value(interactive_save)
|
|
295
|
+
before_click = world_size(interactive_save)
|
|
296
|
+
|
|
297
|
+
_, _, width, height = window_geometry(window_id)
|
|
298
|
+
xdotool(
|
|
299
|
+
"mousemove",
|
|
300
|
+
"--window",
|
|
301
|
+
window_id,
|
|
302
|
+
str(width // 2),
|
|
303
|
+
str(height // 2),
|
|
304
|
+
)
|
|
305
|
+
xdotool("click", "1")
|
|
306
|
+
time.sleep(0.35)
|
|
307
|
+
require_running(process, log_path)
|
|
308
|
+
save_path.unlink(missing_ok=True)
|
|
309
|
+
press(window_id, "s")
|
|
310
|
+
require_running(process, log_path)
|
|
311
|
+
fail_if(
|
|
312
|
+
not save_path.is_file(),
|
|
313
|
+
"Interactive save failed after left-click",
|
|
314
|
+
failures,
|
|
315
|
+
)
|
|
316
|
+
if save_path.is_file():
|
|
317
|
+
after_break = json.loads(save_path.read_text())
|
|
318
|
+
fail_if(
|
|
319
|
+
world_size(after_break) >= before_click,
|
|
320
|
+
"Interactive left-click did not break a targeted block",
|
|
321
|
+
failures,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
xdotool("click", "3")
|
|
325
|
+
time.sleep(0.35)
|
|
326
|
+
require_running(process, log_path)
|
|
327
|
+
save_path.unlink(missing_ok=True)
|
|
328
|
+
press(window_id, "s")
|
|
329
|
+
require_running(process, log_path)
|
|
330
|
+
fail_if(
|
|
331
|
+
not save_path.is_file(),
|
|
332
|
+
"Interactive save failed after right-click",
|
|
333
|
+
failures,
|
|
334
|
+
)
|
|
335
|
+
if save_path.is_file():
|
|
336
|
+
after_place = json.loads(save_path.read_text())
|
|
337
|
+
fail_if(
|
|
338
|
+
world_size(after_place) <= world_size(after_break),
|
|
339
|
+
"Interactive right-click did not place the selected block",
|
|
340
|
+
failures,
|
|
341
|
+
)
|
|
342
|
+
fail_if(
|
|
343
|
+
selected_value(after_place) != selected,
|
|
344
|
+
"Placed-block save lost the selected block state",
|
|
345
|
+
failures,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
movement_failed = changed_pixels(captures["initial"], captures["move"]) < 100
|
|
349
|
+
fail_if(
|
|
350
|
+
movement_failed,
|
|
351
|
+
"Interactive W key did not visibly move the player/camera",
|
|
352
|
+
failures,
|
|
353
|
+
)
|
|
354
|
+
if movement_failed and "FirstPersonController" in entry.read_text():
|
|
355
|
+
failures.append(
|
|
356
|
+
"FirstPersonController movement can be blocked by spawn geometry: "
|
|
357
|
+
"start on terrain with a traversable forward approach before the target."
|
|
358
|
+
)
|
|
359
|
+
fail_if(
|
|
360
|
+
changed_pixels(captures["move"], captures["select"]) < 20,
|
|
361
|
+
"Interactive number key did not visibly change block selection",
|
|
362
|
+
failures,
|
|
363
|
+
)
|
|
364
|
+
fail_if(
|
|
365
|
+
changed_pixels(captures["select"], captures["pause"]) < 100,
|
|
366
|
+
"Interactive P key did not show a paused state",
|
|
367
|
+
failures,
|
|
368
|
+
)
|
|
369
|
+
except Exception as error:
|
|
370
|
+
failures.append(f"Interactive probe failed: {error}")
|
|
371
|
+
finally:
|
|
372
|
+
if process.poll() is None:
|
|
373
|
+
process.terminate()
|
|
374
|
+
try:
|
|
375
|
+
process.wait(timeout=5)
|
|
376
|
+
except subprocess.TimeoutExpired:
|
|
377
|
+
process.kill()
|
|
378
|
+
process.wait(timeout=5)
|
|
379
|
+
callbacks = nested_ursina_callbacks(entry)
|
|
380
|
+
if len(failures) > failure_count and callbacks:
|
|
381
|
+
failures.append(
|
|
382
|
+
"Ursina does not auto-dispatch callbacks nested inside another function: "
|
|
383
|
+
f"{', '.join(callbacks)}. Put them at module scope or on an instantiated Entity."
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def world_size(data):
|
|
388
|
+
if isinstance(data, dict):
|
|
389
|
+
for key in ("world", "blocks", "tiles", "voxels"):
|
|
390
|
+
value = data.get(key)
|
|
391
|
+
if isinstance(value, dict):
|
|
392
|
+
return len(value)
|
|
393
|
+
if isinstance(value, list):
|
|
394
|
+
return len(value)
|
|
395
|
+
return 0
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def selected_value(data):
|
|
399
|
+
if isinstance(data, dict):
|
|
400
|
+
for key in ("selected", "selected_block", "selected_block_index", "active_block"):
|
|
401
|
+
if key in data:
|
|
402
|
+
return data[key]
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def parse_smoke_result(output):
|
|
407
|
+
for line in output.splitlines():
|
|
408
|
+
if line.startswith(MARKER):
|
|
409
|
+
return json.loads(line[len(MARKER):])
|
|
410
|
+
raise Failure(
|
|
411
|
+
f"Missing {MARKER.strip()} JSON line. --smoke must report gameplay evidence, "
|
|
412
|
+
"not just save a screenshot."
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def check_smoke_protocol(result, save_path, screenshot, failures):
|
|
417
|
+
required_true = {
|
|
418
|
+
"spawn_safe": "safe spawn",
|
|
419
|
+
"moved": "WASD/player movement",
|
|
420
|
+
"jumped": "jump or gravity response",
|
|
421
|
+
"looked": "camera/mouselook change",
|
|
422
|
+
"selected_block_changed": "number-key block selection",
|
|
423
|
+
"break_changed_world": "breaking a targeted block",
|
|
424
|
+
"place_changed_world": "placing a selected block",
|
|
425
|
+
"pause_toggled": "pause/resume toggle",
|
|
426
|
+
"save_roundtrip": "save/load round-trip",
|
|
427
|
+
}
|
|
428
|
+
for key, label in required_true.items():
|
|
429
|
+
fail_if(result.get(key) is not True, f"Smoke did not prove {label} ({key}=true)", failures)
|
|
430
|
+
|
|
431
|
+
block_types = result.get("block_type_count") or result.get("selectable_block_types") or 0
|
|
432
|
+
fail_if(
|
|
433
|
+
not isinstance(block_types, int) or block_types < 4,
|
|
434
|
+
"Game needs at least four selectable block types",
|
|
435
|
+
failures,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
before = result.get("world_blocks_before")
|
|
439
|
+
after_break = result.get("world_blocks_after_break")
|
|
440
|
+
after_place = result.get("world_blocks_after_place")
|
|
441
|
+
fail_if(
|
|
442
|
+
not isinstance(before, int) or before < 20,
|
|
443
|
+
"Smoke world must contain a real block field (>=20 blocks)",
|
|
444
|
+
failures,
|
|
445
|
+
)
|
|
446
|
+
fail_if(
|
|
447
|
+
not isinstance(after_break, int) or after_break >= before,
|
|
448
|
+
"Breaking must reduce the world/block count",
|
|
449
|
+
failures,
|
|
450
|
+
)
|
|
451
|
+
fail_if(
|
|
452
|
+
not isinstance(after_place, int) or after_place <= after_break,
|
|
453
|
+
"Placing must add a block after breaking",
|
|
454
|
+
failures,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
fail_if(not save_path.is_file(), "Smoke did not write the requested save file", failures)
|
|
458
|
+
if save_path.is_file():
|
|
459
|
+
try:
|
|
460
|
+
data = json.loads(save_path.read_text())
|
|
461
|
+
except Exception as error:
|
|
462
|
+
failures.append(f"Save file is not valid JSON: {error}")
|
|
463
|
+
else:
|
|
464
|
+
fail_if(
|
|
465
|
+
world_size(data) < 20,
|
|
466
|
+
"Save file does not contain meaningful world progress",
|
|
467
|
+
failures,
|
|
468
|
+
)
|
|
469
|
+
fail_if(
|
|
470
|
+
selected_value(data) in (None, 0, "0", "dirt"),
|
|
471
|
+
"Save file did not preserve changed selected block",
|
|
472
|
+
failures,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
fail_if(not screenshot.is_file(), "Smoke did not write the requested screenshot", failures)
|
|
476
|
+
if screenshot.is_file():
|
|
477
|
+
colors, dark = sampled_colors_and_dark_ratio(screenshot)
|
|
478
|
+
fail_if(
|
|
479
|
+
colors < 8,
|
|
480
|
+
f"Smoke screenshot has too little visual variety ({colors} sampled colors)",
|
|
481
|
+
failures,
|
|
482
|
+
)
|
|
483
|
+
fail_if(
|
|
484
|
+
dark > 0.70,
|
|
485
|
+
"Smoke screenshot is mostly dark/pause overlay, not playable world evidence",
|
|
486
|
+
failures,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def check_docs_and_tests(candidate, failures):
|
|
491
|
+
readmes = [p for p in (candidate / "README.md", candidate / "readme.md") if p.is_file()]
|
|
492
|
+
fail_if(
|
|
493
|
+
not readmes,
|
|
494
|
+
"Missing README with setup, run command, controls, and save/load notes",
|
|
495
|
+
failures,
|
|
496
|
+
)
|
|
497
|
+
if readmes:
|
|
498
|
+
text = readmes[0].read_text(errors="replace").lower()
|
|
499
|
+
for needle in ("python", "minecraft.py", "pause", "save"):
|
|
500
|
+
fail_if(needle not in text, f"README missing {needle!r} instruction", failures)
|
|
501
|
+
fail_if(
|
|
502
|
+
not re.search(r"\bwasd\b|w\s*/\s*a\s*/\s*s\s*/\s*d", text),
|
|
503
|
+
"README missing WASD movement instructions",
|
|
504
|
+
failures,
|
|
505
|
+
)
|
|
506
|
+
fail_if(
|
|
507
|
+
"pytest" in text and not importlib.util.find_spec("pytest"),
|
|
508
|
+
"README asks for pytest but pytest is not installed",
|
|
509
|
+
failures,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
tests = candidate / "tests"
|
|
513
|
+
fail_if(
|
|
514
|
+
not any(tests.glob("test*.py")) if tests.is_dir() else True,
|
|
515
|
+
"Missing Python tests under tests/",
|
|
516
|
+
failures,
|
|
517
|
+
)
|
|
518
|
+
if tests.is_dir() and any(tests.glob("test*.py")):
|
|
519
|
+
result = run([sys.executable, "-m", "unittest", "discover", "-s", "tests"], candidate, timeout=30)
|
|
520
|
+
fail_if(result.returncode != 0, f"Candidate tests failed under unittest:\n{result.stdout[-2000:]}", failures)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def check_static(entry, failures):
|
|
524
|
+
text = entry.read_text(errors="replace")
|
|
525
|
+
lowered = text.lower()
|
|
526
|
+
for token, label in [
|
|
527
|
+
("--smoke", "smoke CLI"),
|
|
528
|
+
("--screenshot", "screenshot CLI"),
|
|
529
|
+
("save", "save/load code"),
|
|
530
|
+
("pause", "pause code"),
|
|
531
|
+
]:
|
|
532
|
+
fail_if(token not in lowered, f"minecraft.py missing {label}", failures)
|
|
533
|
+
fail_if(
|
|
534
|
+
not re.search(r"if\s+__name__\s*==\s*['\"]__main__['\"]", text),
|
|
535
|
+
"minecraft.py must use a main guard",
|
|
536
|
+
failures,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def main():
|
|
541
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
542
|
+
parser.add_argument("candidate", help="Candidate work directory")
|
|
543
|
+
parser.add_argument("--probe", required=True, help="Path to verify_ursina.py or compatible renderer probe")
|
|
544
|
+
parser.add_argument("--artifacts", default="artifacts", help="Artifact directory relative to the candidate")
|
|
545
|
+
parser.add_argument("--skip-interactive", action="store_true", help=argparse.SUPPRESS)
|
|
546
|
+
args = parser.parse_args()
|
|
547
|
+
|
|
548
|
+
candidate = Path(args.candidate).resolve()
|
|
549
|
+
entry = candidate / "minecraft.py"
|
|
550
|
+
artifacts = candidate / args.artifacts
|
|
551
|
+
artifacts.mkdir(parents=True, exist_ok=True)
|
|
552
|
+
failures = []
|
|
553
|
+
|
|
554
|
+
fail_if(not entry.is_file(), "Missing minecraft.py launcher", failures)
|
|
555
|
+
if failures:
|
|
556
|
+
raise SystemExit("\n".join(failures))
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
import_safely(entry)
|
|
560
|
+
except SystemExit as error:
|
|
561
|
+
failures.append(f"Importing minecraft.py exited the process: {error}")
|
|
562
|
+
except Exception as error:
|
|
563
|
+
failures.append(str(error))
|
|
564
|
+
check_static(entry, failures)
|
|
565
|
+
check_docs_and_tests(candidate, failures)
|
|
566
|
+
|
|
567
|
+
probe_shot = artifacts / "grade-probe.png"
|
|
568
|
+
probe = run(
|
|
569
|
+
[sys.executable, str(Path(args.probe).resolve()), "minecraft.py", "--screenshot", str(probe_shot)],
|
|
570
|
+
candidate,
|
|
571
|
+
timeout=30,
|
|
572
|
+
)
|
|
573
|
+
fail_if(
|
|
574
|
+
probe.returncode != 0 or "URSINA_PROBE_RESULT " not in probe.stdout,
|
|
575
|
+
f"Renderer probe failed:\n{probe.stdout[-2000:]}",
|
|
576
|
+
failures,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
smoke_shot = artifacts / "grade-smoke.png"
|
|
580
|
+
smoke_save = artifacts / "grade-save.json"
|
|
581
|
+
smoke = run(
|
|
582
|
+
[sys.executable, "minecraft.py", "--smoke", "--screenshot", str(smoke_shot), "--save", str(smoke_save)],
|
|
583
|
+
candidate,
|
|
584
|
+
timeout=30,
|
|
585
|
+
)
|
|
586
|
+
fail_if(smoke.returncode != 0, f"Candidate smoke failed:\n{smoke.stdout[-2000:]}", failures)
|
|
587
|
+
if smoke.returncode == 0:
|
|
588
|
+
try:
|
|
589
|
+
result = parse_smoke_result(smoke.stdout)
|
|
590
|
+
check_smoke_protocol(result, smoke_save, smoke_shot, failures)
|
|
591
|
+
except Exception as error:
|
|
592
|
+
failures.append(str(error))
|
|
593
|
+
if not args.skip_interactive:
|
|
594
|
+
check_interactive_window(candidate, entry, artifacts, failures)
|
|
595
|
+
|
|
596
|
+
if failures:
|
|
597
|
+
print("GAME_GRADE_RESULT " + json.dumps({"status": "failed", "failures": failures}, indent=2))
|
|
598
|
+
raise SystemExit(1)
|
|
599
|
+
print("GAME_GRADE_RESULT " + json.dumps({"status": "passed", "candidate": str(candidate)}))
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
if __name__ == "__main__":
|
|
603
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "game",
|
|
3
|
+
"description": "Minecraft-style Ursina game starting from a verified renderer slice, graded headless and in a real window",
|
|
4
|
+
"prompt": "Build me a simple Minecraft-style game in Python. I should be able to explore the world, break and place different blocks, pause, and save my progress. Make it feel complete enough to play, and include tests and setup instructions.",
|
|
5
|
+
"setup": "Ursina and Pillow are already installed in the supplied Python. External renderer probe: {root}/skills/game-development/scripts/verify_ursina.py. Run it early against minecraft.py and again after renderer changes; the required check uses the same probe. Preserve the seeded renderer lifecycle: default launch must call app.run() and stay interactive, while app.destroy() belongs only after the bounded --smoke path. Normal-window acceptance drives the public keyboard and mouse paths and rejects smoke-only state changes, so attach input/update methods to a live Entity. Keep the initial positive-Z movement path on terrain and clear while leaving a target reachable afterward.",
|
|
6
|
+
"skills": ["game-development"],
|
|
7
|
+
"context": ["context.md"],
|
|
8
|
+
"seeds": [
|
|
9
|
+
{ "from": "{root}/skills/game-development/assets/ursina_starter.py", "to": "minecraft.py" }
|
|
10
|
+
],
|
|
11
|
+
"developmentChecks": [
|
|
12
|
+
{
|
|
13
|
+
"id": "acceptance_core",
|
|
14
|
+
"kind": "runtime",
|
|
15
|
+
"argv": ["{python}", "{scenario}/grade_minecraft_clone.py", "{cwd}", "--probe", "{root}/skills/game-development/scripts/verify_ursina.py", "--skip-interactive"],
|
|
16
|
+
"timeoutSeconds": 90
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "acceptance_interactive",
|
|
20
|
+
"kind": "runtime",
|
|
21
|
+
"argv": ["{python}", "{scenario}/grade_minecraft_clone.py", "{cwd}", "--probe", "{root}/skills/game-development/scripts/verify_ursina.py"],
|
|
22
|
+
"timeoutSeconds": 120
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"holdoutChecks": [
|
|
26
|
+
{
|
|
27
|
+
"id": "acceptance_holdout",
|
|
28
|
+
"kind": "runtime",
|
|
29
|
+
"argv": ["{python}", "{scenario}/grade_minecraft_clone.py", "{cwd}", "--probe", "{root}/skills/game-development/scripts/verify_ursina.py"],
|
|
30
|
+
"timeoutSeconds": 120
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|