@pop-party/create-game 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -19,6 +19,11 @@ the configured immutable release; later runs keep that independent local copy.
19
19
  Production never reads the development workspace. Both commands validate the
20
20
  complete selected release before opening a port.
21
21
 
22
+ The resulting service immediately receives the engine-owned stage, controller,
23
+ room lifecycle, and authenticated core tools. Its `src/stage`,
24
+ `src/controller`, and `src/tools` folders begin as empty additive plugin
25
+ boundaries; they do not contain placeholder pages or copies of engine code.
26
+
22
27
  `pop-party migrate` previews the registered, contiguous game migration path.
23
28
  It writes nothing unless `--output <new-directory>` is supplied, never mutates
24
29
  the source bundle, runs every migration twice to reject nondeterministic output,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pop-party/create-game",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Create an independent Pop Party game pinned to an exact engine version.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
 
3
+ const { version: DEFAULT_ENGINE_VERSION } = require("../package.json");
4
+
3
5
  const HELP_TEXT = [
4
6
  "Usage: npm create @pop-party/game <name> [options]",
5
7
  "",
@@ -38,7 +40,7 @@ function parseCreateGameArguments(argv = []) {
38
40
  if (!displayName) throw new Error("Game display name is required");
39
41
  return Object.freeze({
40
42
  displayName,
41
- engineVersion: options.engineVersion || "1.0.0",
43
+ engineVersion: options.engineVersion || DEFAULT_ENGINE_VERSION,
42
44
  starterRoot: options.starterRoot || undefined,
43
45
  targetRoot: options.targetRoot || undefined
44
46
  });
@@ -55,11 +55,6 @@ function generatedContributionSource(kind) {
55
55
  return `"use strict";\n\n// Add ${kind} contributions as { id: "game-namespace.name", value: ... }.\nmodule.exports = Object.freeze([]);\n`;
56
56
  }
57
57
 
58
- function generatedBootstrapRendererSource({ gameId, role }) {
59
- const kind = role === "stage" ? "Stage" : "Controller";
60
- return `"use strict";\n\nmodule.exports = Object.freeze([{\n id: ${JSON.stringify(`${gameId}.bootstrap-${role}`)},\n value: Object.freeze({\n renderBootstrap({ game }) {\n if (game.id !== ${JSON.stringify(gameId)}) throw new Error("${kind} renderer received another game");\n return Object.freeze({\n heading: game.displayName,\n message: ${JSON.stringify(`${kind} service is ready.`)}\n });\n }\n })\n}]);\n`;
61
- }
62
-
63
58
  function generatedRenderBlueprintSource(gameId) {
64
59
  return `services:\n - type: web\n name: ${gameId}\n runtime: node\n plan: starter\n numInstances: 1\n buildCommand: npm install --no-audit --no-fund && npm run build\n startCommand: npm start\n healthCheckPath: /health\n maxShutdownDelaySeconds: 300\n envVars:\n - key: NODE_ENV\n value: production\n`;
65
60
  }
@@ -69,7 +64,7 @@ function generatedPluginSource(pluginNamespace) {
69
64
  }
70
65
 
71
66
  function generatedConfigTestSource({ engineVersion, gameId, pluginNamespace }) {
72
- return `"use strict";\n\nconst assert = require("node:assert/strict");\nconst test = require("node:test");\nconst { createGameApplicationRuntime } = require("@pop-party/engine/server/application");\nconst { createGameReadinessRuntime } = require("@pop-party/engine/server/readiness");\nconst game = require("../game.config");\n\ntest("game configuration owns an exact engine and plugin boundary", async () => {\n assert.equal(game.gameId, ${JSON.stringify(gameId)});\n assert.equal(game.engineCompatibility, ${JSON.stringify(engineVersion)});\n assert.equal(game.plugin.namespace, ${JSON.stringify(pluginNamespace)});\n assert.equal(game.gameData, null);\n assert.equal(game.registrations.stageRenderers.length, 1);\n assert.equal(game.registrations.stageRenderers[0].id, ${JSON.stringify(`${gameId}.bootstrap-stage`)});\n assert.equal(game.registrations.controllerRenderers.length, 1);\n assert.equal(game.registrations.controllerRenderers[0].id, ${JSON.stringify(`${gameId}.bootstrap-controller`)});\n assert.deepEqual({\n actions: game.registrations.actions,\n stateSchemas: game.registrations.stateSchemas,\n validators: game.registrations.validators,\n migrations: game.registrations.migrations,\n toolPanels: game.registrations.toolPanels,\n diagnostics: game.registrations.diagnostics\n }, {\n actions: [],\n stateSchemas: [],\n validators: [],\n migrations: [],\n toolPanels: [],\n diagnostics: []\n });\n const readiness = createGameReadinessRuntime({ gameDefinition: game, engineVersion: ${JSON.stringify(engineVersion)} });\n const active = await readiness.check();\n assert.equal(active.release.gameId, ${JSON.stringify(gameId)});\n assert.equal(active.release.engineVersion, ${JSON.stringify(engineVersion)});\n assert.ok(active.gameData.defaultGameFlow.states.length > 0);\n assert.ok(active.gameData.defaultArtCompositions.length > 0);\n assert.equal(readiness.state.status, "ready");\n});\n\ntest("generated game starts as an independent validated web service", async () => {\n const runtime = createGameApplicationRuntime({\n gameDefinition: game,\n engineVersion: ${JSON.stringify(engineVersion)},\n host: "127.0.0.1",\n port: 0\n });\n try {\n const startup = await runtime.start();\n const health = await fetch(\`${"${startup.localUrl}"}/health\`);\n const stage = await fetch(\`${"${startup.localUrl}"}/stage\`);\n const controller = await fetch(\`${"${startup.localUrl}"}/controller\`);\n const tools = await fetch(\`${"${startup.localUrl}"}/tools\`);\n assert.equal(health.status, 200);\n assert.equal((await health.json()).release.contentRevision, runtime.active.release.contentRevision);\n assert.equal(stage.status, 200);\n assert.match(await stage.text(), /data-pop-party-role="stage"/);\n assert.equal(controller.status, 200);\n assert.match(await controller.text(), /data-pop-party-role="controller"/);\n assert.equal(tools.status, 503);\n assert.equal((await tools.json()).diagnostic.code, "GAME_TOOLING_NOT_CONFIGURED");\n } finally {\n await runtime.stop();\n }\n});\n`;
67
+ return `"use strict";\n\nconst assert = require("node:assert/strict");\nconst test = require("node:test");\nconst { createGameApplicationRuntime } = require("@pop-party/engine/server/application");\nconst { createGameReadinessRuntime } = require("@pop-party/engine/server/readiness");\nconst game = require("../game.config");\n\ntest("game configuration owns an exact engine and plugin boundary", async () => {\n assert.equal(game.gameId, ${JSON.stringify(gameId)});\n assert.equal(game.engineCompatibility, ${JSON.stringify(engineVersion)});\n assert.equal(game.plugin.namespace, ${JSON.stringify(pluginNamespace)});\n assert.equal(game.gameData, null);\n assert.deepEqual({\n actions: game.registrations.actions,\n stageRenderers: game.registrations.stageRenderers,\n controllerRenderers: game.registrations.controllerRenderers,\n stateSchemas: game.registrations.stateSchemas,\n validators: game.registrations.validators,\n migrations: game.registrations.migrations,\n toolPanels: game.registrations.toolPanels,\n diagnostics: game.registrations.diagnostics\n }, {\n actions: [],\n stageRenderers: [],\n controllerRenderers: [],\n stateSchemas: [],\n validators: [],\n migrations: [],\n toolPanels: [],\n diagnostics: []\n });\n const readiness = createGameReadinessRuntime({ gameDefinition: game, engineVersion: ${JSON.stringify(engineVersion)} });\n const active = await readiness.check();\n assert.equal(active.release.gameId, ${JSON.stringify(gameId)});\n assert.equal(active.release.engineVersion, ${JSON.stringify(engineVersion)});\n assert.ok(active.gameData.defaultGameFlow.states.length > 0);\n assert.ok(active.gameData.defaultArtCompositions.length > 0);\n assert.equal(readiness.state.status, "ready");\n});\n\ntest("generated game starts with the complete engine-owned application", async () => {\n const runtime = createGameApplicationRuntime({\n gameDefinition: game,\n engineVersion: ${JSON.stringify(engineVersion)},\n workspaceRoot: process.cwd(),\n host: "127.0.0.1",\n port: 0\n });\n try {\n const startup = await runtime.start();\n const health = await fetch(\`${"${startup.localUrl}"}/health\`);\n const stage = await fetch(\`${"${startup.localUrl}"}/stage\`);\n const controller = await fetch(\`${"${startup.localUrl}"}/controller\`);\n const tools = await fetch(\`${"${startup.localUrl}"}/tools\`);\n const flow = await fetch(\`${"${startup.localUrl}"}/api/game-flow\`);\n assert.equal(health.status, 200);\n assert.equal((await health.json()).game.id, ${JSON.stringify(gameId)});\n assert.equal(stage.status, 200);\n assert.match(await stage.text(), /id="stageScreen"/);\n assert.equal(controller.status, 200);\n assert.match(await controller.text(), /id="controllerScreen"/);\n assert.equal(tools.status, 200);\n assert.match(await tools.text(), /id="toolDashboardBar"/);\n assert.equal(flow.status, 200);\n assert.ok((await flow.json()).flow.states.length > 0);\n } finally {\n await runtime.stop();\n }\n});\n`;
73
68
  }
74
69
 
75
70
  function rewriteBundleGameId(contentRoot, gameId) {
@@ -115,13 +110,13 @@ function generateGame(options = {}) {
115
110
  writeText(stagingRoot, "package.json", `${JSON.stringify(manifest, null, 2)}\n`);
116
111
  writeText(stagingRoot, "game.config.js", generatedConfigSource({ displayName, engineVersion, gameId }));
117
112
  writeText(stagingRoot, "src/actions/index.js", generatedContributionSource("server and flow action"));
118
- writeText(stagingRoot, "src/stage/index.js", generatedBootstrapRendererSource({ gameId, role: "stage" }));
119
- writeText(stagingRoot, "src/controller/index.js", generatedBootstrapRendererSource({ gameId, role: "controller" }));
113
+ writeText(stagingRoot, "src/stage/index.js", generatedContributionSource("stage renderer"));
114
+ writeText(stagingRoot, "src/controller/index.js", generatedContributionSource("controller renderer"));
120
115
  writeText(stagingRoot, "src/tools/index.js", generatedContributionSource("authenticated tool panel"));
121
116
  writeText(stagingRoot, "src/plugin/index.js", generatedPluginSource(pluginNamespace));
122
117
  writeText(stagingRoot, "tests/config.test.js", generatedConfigTestSource({ engineVersion, gameId, pluginNamespace }));
123
118
  writeText(stagingRoot, ".gitignore", "node_modules/\n.env\n.pop-party/\ndist/\noutputs/\n");
124
- writeText(stagingRoot, "README.md", `# ${displayName}\n\nIndependent Pop Party game using \`@pop-party/engine@${engineVersion}\`.\n\nRun \`npm run dev\` locally or \`npm start\` in production. Development seeds ignored \`.pop-party/content\` once and then preserves that independent local copy; production uses the configured active store. The engine validates the selected release before binding the service port. Game-owned contributions live under \`src/actions\`, \`src/stage\`, \`src/controller\`, and \`src/tools\`; register them through the namespaced plugin in \`src/plugin\`. Content and starter blobs under \`content\` are independent copies owned by this game. Authenticated tools fail closed until the game explicitly configures them.\n`);
119
+ writeText(stagingRoot, "README.md", `# ${displayName}\n\nIndependent Pop Party game using \`@pop-party/engine@${engineVersion}\`.\n\nRun \`npm run dev\` locally or \`npm start\` in production. Development seeds ignored \`.pop-party/content\` once and then preserves that independent local copy; production uses the configured active store. The engine validates the selected release before binding the service port and supplies the complete stage, controller, room lifecycle, and authenticated core tools. Game-owned contributions live under \`src/actions\`, \`src/stage\`, \`src/controller\`, and \`src/tools\`; register them through the namespaced plugin in \`src/plugin\`. Content and starter blobs under \`content\` are independent copies owned by this game.\n`);
125
120
  writeText(stagingRoot, "render.yaml", generatedRenderBlueprintSource(gameId));
126
121
  writeText(stagingRoot, "DEPLOYMENT.md", `# ${displayName} deployment\n\nThis game deploys as one independent Render web service defined by \`render.yaml\`. Keep \`numInstances: 1\` and autoscaling disabled while rooms are in-memory. Render builds with \`npm install --no-audit --no-fund && npm run build\`, starts with \`npm start\`, and checks \`/health\`.\n\nProduction must use reviewed provider credentials and an immutable active release. Do not point \`POP_PARTY_CONTENT_ROOT\` at \`.pop-party/content\`; that override is only for the engine-owned local development command. Configure provider, OAuth, and content-writer secrets in Render rather than committing them.\n`);
127
122
  writeText(stagingRoot, "LICENSE", fs.readFileSync(path.join(__dirname, "..", "LICENSE"), "utf8"));