diff --git a/app/lib/icemake.ts b/app/lib/icemake.ts index 5c439bd..827d7dc 100644 --- a/app/lib/icemake.ts +++ b/app/lib/icemake.ts @@ -8,7 +8,7 @@ export type ComponentGraphNode = { export type ExecutionStep = { componentIndex: number; - stack: Flavor[]; + stack: Flavor[] | null; // コーンが食べられたらnull branchTaken?: boolean; }; @@ -17,6 +17,7 @@ export type IcemakeResult = { traces: { color: ConeColor; steps: ExecutionStep[] }[]; }; +// 1つのコーンに積めるアイスの最大数(UI上の想定に合わせた安全上限) const MAX_ICE_STACK_SIZE = 10; export function icemake( @@ -28,6 +29,7 @@ export function icemake( const stageData = STAGES[stage]; if (!stageData) return { result: {}, traces: [] }; + // グラフが渡されたときは、各色ごとに実行トレースを取りながら結果を計算する if (graph && firstComponentId !== undefined) { const traces = colors.map((color) => { const steps = runGraphExecution( @@ -38,16 +40,18 @@ export function icemake( ); return { color, steps }; }); + // 最終ステップのstackをその色の完成結果として採用する const result = Object.fromEntries( traces.map(({ color, steps }) => [ color, - steps.length > 0 ? steps[steps.length - 1].stack : [], + steps.length > 0 ? (steps[steps.length - 1].stack ?? []) : [], ]), ); return { result, traces }; } return { + // グラフ未実行時はステージ定義のミッションを既定値として返す result: Object.fromEntries( colors.map((color) => [color, stageData.mission[color] ?? []]), ), @@ -61,8 +65,10 @@ function runGraphExecution( graph: Record, firstComponentId: number, ): ExecutionStep[] { - const stack: Flavor[] = []; + // コーンが食べられたらnull + let stack: Flavor[] | null = []; const steps: ExecutionStep[] = []; + // 同じノード + 同じstack状態に再訪したら無限ループとみなして停止する const visited: Map> = new Map(); let currentId: number | null = firstComponentId; @@ -80,14 +86,17 @@ function runGraphExecution( if (!componentVisited || componentVisited.has(stackKey)) break; componentVisited.add(stackKey); + if (!stack) break; + switch (component.type) { case "push": if (stack.length < MAX_ICE_STACK_SIZE) stack.push(component.flavor); break; case "pop": - if ( - stack.length > 0 && - (!component.flavor || stack[stack.length - 1] === component.flavor) + if (stack.length === 0) stack = null; + else if ( + !component.flavor || + stack[stack.length - 1] === component.flavor ) stack.pop(); break; @@ -95,43 +104,58 @@ function runGraphExecution( break; } - const children: ComponentGraphNode["childrenIds"] = node.childrenIds; - - let branchTaken: boolean | undefined; - if (children != null && typeof children !== "number") { - let condition = false; - if (component.type === "if") { - const cond: ConeColor | Flavor | Flavor[] | number = - component.condition; - if (typeof cond === "string") { - if (coneColors.includes(cond as ConeColor)) - condition = color === cond; - else if (flavors.includes(cond as Flavor)) - condition = stack.length > 0 && stack[stack.length - 1] === cond; - } else if (Array.isArray(cond)) { - for (let i = 0; i <= stack.length - cond.length; i++) { - if ( - stack.slice(i, i + cond.length).every((f, j) => f === cond[j]) - ) { - condition = true; - break; + if (!stack) { + steps.push({ + componentIndex: currentId, + stack: null, + branchTaken: undefined, + }); + } else { + const children: ComponentGraphNode["childrenIds"] = node.childrenIds; + + let branchTaken: boolean | undefined; + if (children != null && typeof children !== "number") { + let condition = false; + // ifノードの条件を、色 / 先頭フレーバー / 部分配列一致 / 個数で評価する + if (component.type === "if") { + const cond: ConeColor | Flavor | Flavor[] | number = + component.condition; + if (typeof cond === "string") { + if (coneColors.includes(cond as ConeColor)) + condition = color === cond; + else if (flavors.includes(cond as Flavor)) + condition = stack.length > 0 && stack[stack.length - 1] === cond; + } else if (Array.isArray(cond)) { + for (let i = 0; i <= stack.length - cond.length; i++) { + if ( + stack.slice(i, i + cond.length).every((f, j) => f === cond[j]) + ) { + condition = true; + break; + } } + } else if (typeof cond === "number") { + condition = stack.length >= cond; } - } else if (typeof cond === "number") { - condition = stack.length >= cond; } + branchTaken = condition; } - branchTaken = condition; - } - steps.push({ componentIndex: currentId, stack: [...stack], branchTaken }); + steps.push({ + componentIndex: currentId, + stack: stack === null ? null : [...stack], + branchTaken, + }); - if (children == null) break; + if (children == null) break; - if (typeof children === "number") { - currentId = children; - } else { - currentId = branchTaken ? children.true : children.false; + if (typeof children === "number") { + // 直線接続 + currentId = children; + } else { + // 分岐接続(true/false) + currentId = branchTaken ? children.true : children.false; + } } } diff --git a/app/routes/stage.$id.tsx b/app/routes/stage.$id.tsx index 7cc6886..306685c 100644 --- a/app/routes/stage.$id.tsx +++ b/app/routes/stage.$id.tsx @@ -113,7 +113,15 @@ function getComponentSrc( return { src: "" }; } -function ConeStack({ color, stack }: { color: ConeColor; stack: Flavor[] }) { +function ConeStack({ + color, + stack, +}: { + color: ConeColor; + stack: Flavor[] | null; +}) { + if (stack === null) return null; + return (
@@ -154,11 +162,16 @@ function StraightNode({ data }: NodeProps) {
)} - - + )} + {isClear ? ( + nextStageExists && ( + + のステージへ → + + ) + ) : ( + + )}
{/* Fail message */} @@ -1759,9 +1817,24 @@ function StageInner({ )} {/* Clear overlay */} - {isClear && ( + {showClear && (
-
+
+
クリア!
diff --git a/app/stages.ts b/app/stages.ts index 8813df6..21a3e30 100644 --- a/app/stages.ts +++ b/app/stages.ts @@ -12,6 +12,60 @@ export type StageData = { export const STAGES: Record = { 1: { + mission: { + red: ["vanilla"], + }, + components: [{ type: "push", flavor: "vanilla" }], + }, + 2: { + mission: { + red: ["chocolate", "vanilla", "strawberry"], + }, + components: [ + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + ], + }, + 3: { + mission: { + red: ["strawberry", "vanilla"], + yellow: ["strawberry", "chocolate"], + }, + components: [ + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: "red" }, + ], + }, + 4: { + mission: { + red: ["vanilla"], + yellow: ["chocolate"], + brown: ["strawberry"], + }, + components: [ + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: "red" }, + { type: "if", condition: "brown" }, + ], + }, + 5: { + mission: { + red: ["vanilla", "chocolate", "strawberry"], + yellow: ["chocolate", "strawberry"], + }, + components: [ + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: "red" }, + ], + }, + 6: { mission: { red: ["vanilla", "chocolate"], yellow: ["strawberry", "vanilla"], @@ -25,49 +79,49 @@ export const STAGES: Record = { { type: "pop", flavor: undefined }, ], }, - 2: { + 7: { mission: { red: ["chocolate"], yellow: ["vanilla", "strawberry"], - brown: ["vanilla", "chocolate"] + brown: ["vanilla", "chocolate"], }, components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "push", flavor: "strawberry"}, - {type: "if", condition: "red"}, - {type: "if", condition: "yellow"}, - {type: "pop", flavor: undefined}, + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: "red" }, + { type: "if", condition: "yellow" }, + { type: "pop", flavor: undefined }, ], }, - 3: { + 8: { mission: { - red: ["chocolate", "vanilla", "chocolate"], - yellow: ["vanilla", "vanilla", "vanilla"], + red: ["vanilla", "vanilla", "vanilla"], + yellow: ["chocolate", "chocolate", "chocolate"], + brown: ["strawberry", "strawberry", "strawberry"], }, components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "if", condition: "yellow"}, - {type: "if", condition: 3}, - + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: "red" }, + { type: "if", condition: "brown" }, + { type: "if", condition: 3 }, ], }, - 4: { + 9: { mission: { - red: ["vanilla", "chocolate", "vanilla", "chocolate"], - yellow: ["chocolate", "vanilla", "chocolate", "chocolate"], + red: ["chocolate", "vanilla", "chocolate"], + yellow: ["vanilla", "vanilla", "vanilla"], }, components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "push", flavor: "chocolate"}, - {type: "if", condition: "yellow"}, - {type: "if", condition: 4}, - + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "if", condition: "yellow" }, + { type: "if", condition: 3 }, ], }, - 11: { + 10: { mission: { red: ["strawberry", "vanilla", "strawberry", "vanilla", "vanilla"], yellow: ["strawberry", "vanilla", "strawberry", "vanilla", "chocolate"], @@ -81,102 +135,68 @@ export const STAGES: Record = { { type: "if", condition: "red" }, ], }, - 12: { + 11: { mission: { - red: ["vanilla", "chocolate", "vanilla"], - yellow: ["vanilla", "vanilla", "chocolate"] + red: ["vanilla", "chocolate", "vanilla", "chocolate"], + yellow: ["chocolate", "vanilla", "chocolate", "chocolate"], }, components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "push", flavor: "chocolate"}, - {type: "if", condition: "red"}, - {type: "if", condition: ["vanilla", "vanilla"]}, - {type: "pop", flavor: "chocolate"} - ] + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "chocolate" }, + { type: "if", condition: "yellow" }, + { type: "if", condition: 4 }, + ], }, - 13: { + 12: { mission: { red: ["chocolate", "strawberry", "vanilla"], - yellow: [ "strawberry", "vanilla","chocolate"], - brown: [ "vanilla","chocolate", "strawberry"], + yellow: ["strawberry", "vanilla", "chocolate"], + brown: ["vanilla", "chocolate", "strawberry"], }, components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "push", flavor: "strawberry"}, - {type: "if", condition: "red"}, - {type: "if", condition: "yellow"}, - {type: "if", condition: 3}, - {type: "if", condition: 3}, - {type: "if", condition: 3}, - ] + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: "red" }, + { type: "if", condition: "yellow" }, + { type: "if", condition: 3 }, + { type: "if", condition: 3 }, + { type: "if", condition: 3 }, + ], }, 14: { mission: { - red: ["vanilla", "vanilla", "vanilla"], - yellow: ["chocolate", "chocolate", "chocolate"], - brown: ["strawberry", "strawberry", "strawberry"] - }, - components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "push", flavor: "strawberry"}, - {type: "if", condition: "red"}, - {type: "if", condition: "brown"}, - {type: "if", condition: 3} - ] -}, 15: { - mission: { - red: ["chocolate", "strawberry", "chocolate", "vanilla"], - yellow: ["chocolate", "strawberry", "chocolate", "strawberry", "chocolate", "strawberry", "vanilla"] - }, - components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "push", flavor: "strawberry"}, - {type: "if", condition: 6}, - {type: "if", condition: "red"}, - {type: "if", condition: ["strawberry", "chocolate", "strawberry"]}, - {type: "pop", flavor: undefined} - ] -}, - 16: { - mission: { - red: ["chocolate", "strawberry" ,"chocolate", "strawberry", "chocolate", "strawberry"], - yellow: ["strawberry", "chocolate", "vanilla", "strawberry"], - brown: ["chocolate", "vanilla", "strawberry"] + red: ["vanilla", "chocolate", "vanilla"], + yellow: ["vanilla", "vanilla", "chocolate"], }, components: [ - {type: "push", flavor: "vanilla"}, - {type: "push", flavor: "chocolate"}, - {type: "push", flavor: "strawberry"}, - {type: "if", condition: "red"}, - {type: "if", condition: "yellow"}, - {type: "if", condition: ["vanilla"]}, - {type: "if", condition: 5}, - ] -}, - 21: { + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "chocolate" }, + { type: "if", condition: "red" }, + { type: "if", condition: ["vanilla", "vanilla"] }, + { type: "pop", flavor: undefined }, + ], + }, + 15: { mission: { - red: ["chocolate", "vanilla", "strawberry", "chocolate", "vanilla", "strawberry"], - yellow: ["vanilla", "strawberry", "chocolate", "vanilla", "strawberry", "chocolate"], - brown: ["strawberry", "chocolate", "vanilla", "strawberry", "chocolate", "vanilla"], + red: ["chocolate", "vanilla", "chocolate", "strawberry"], + yellow: ["vanilla", "strawberry", "chocolate"], }, components: [ - { type: "if", condition: "yellow" }, { type: "if", condition: "red" }, - { type: "if", condition: 6 }, - { type: "if", condition: 7 }, + { type: "if", condition: 3 }, + { type: "if", condition: ["chocolate", "vanilla"] }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "chocolate" }, { type: "push", flavor: "chocolate" }, { type: "push", flavor: "strawberry" }, { type: "push", flavor: "vanilla" }, - { type: "pop", flavor: "chocolate" }, - { type: "pop", flavor: "vanilla" }, ], }, - 22: { + 16: { mission: { red: ["chocolate", "vanilla", "strawberry", "vanilla"], yellow: ["strawberry", "vanilla", "chocolate", "vanilla"], @@ -190,20 +210,89 @@ export const STAGES: Record = { { type: "push", flavor: "vanilla" }, ], }, - 23: { + 17: { mission: { - red: ["chocolate", "vanilla", "chocolate", "strawberry"], - yellow: ["vanilla","strawberry", "chocolate"], + red: ["chocolate", "strawberry", "chocolate", "vanilla"], + yellow: [ + "chocolate", + "strawberry", + "chocolate", + "strawberry", + "chocolate", + "strawberry", + "vanilla", + ], }, components: [ - { type: "if", condition: "red" }, - { type: "if", condition: 3 }, - { type: "if", condition: ["chocolate", "vanilla"] }, - { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "vanilla" }, { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: 6 }, + { type: "if", condition: "red" }, + { type: "if", condition: ["strawberry", "chocolate", "strawberry"] }, + { type: "pop", flavor: undefined }, + ], + }, + 18: { + mission: { + red: [ + "chocolate", + "vanilla", + "strawberry", + "chocolate", + "vanilla", + "strawberry", + ], + yellow: [ + "vanilla", + "strawberry", + "chocolate", + "vanilla", + "strawberry", + "chocolate", + ], + brown: [ + "strawberry", + "chocolate", + "vanilla", + "strawberry", + "chocolate", + "vanilla", + ], + }, + components: [ + { type: "if", condition: "yellow" }, + { type: "if", condition: "red" }, + { type: "if", condition: 6 }, + { type: "if", condition: 7 }, { type: "push", flavor: "chocolate" }, { type: "push", flavor: "strawberry" }, { type: "push", flavor: "vanilla" }, + { type: "pop", flavor: "chocolate" }, + { type: "pop", flavor: "vanilla" }, + ], + }, + 19: { + mission: { + red: [ + "chocolate", + "strawberry", + "chocolate", + "strawberry", + "chocolate", + "strawberry", + ], + yellow: ["strawberry", "chocolate", "vanilla", "strawberry"], + brown: ["chocolate", "vanilla", "strawberry"], + }, + components: [ + { type: "push", flavor: "vanilla" }, + { type: "push", flavor: "chocolate" }, + { type: "push", flavor: "strawberry" }, + { type: "if", condition: "red" }, + { type: "if", condition: "yellow" }, + { type: "if", condition: ["vanilla"] }, + { type: "if", condition: 5 }, ], }, };