Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 57 additions & 39 deletions app/lib/icemake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type ComponentGraphNode = {

export type ExecutionStep = {
componentIndex: number;
stack: Flavor[];
stack: Flavor[] | null; // コーンが食べられたらnull
branchTaken?: boolean;
};

Expand All @@ -17,6 +17,7 @@ export type IcemakeResult = {
traces: { color: ConeColor; steps: ExecutionStep[] }[];
};

// 1つのコーンに積めるアイスの最大数(UI上の想定に合わせた安全上限)
const MAX_ICE_STACK_SIZE = 10;

export function icemake(
Expand All @@ -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(
Expand All @@ -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] ?? []]),
),
Expand All @@ -61,14 +65,13 @@ function runGraphExecution(
graph: Record<number, ComponentGraphNode>,
firstComponentId: number,
): ExecutionStep[] {
const stack: Flavor[] = [];
// コーンが食べられたらnull
let stack: Flavor[] | null = [];
const steps: ExecutionStep[] = [];
// 同じノード + 同じstack状態に再訪したら無限ループとみなして停止する
const visited: Map<number, Set<string>> = new Map();
let currentId: number | null = firstComponentId;

const coneColors: ConeColor[] = ["red", "yellow", "brown"];
const flavors: Flavor[] = ["vanilla", "chocolate", "strawberry"];

while (currentId !== null) {
const component: Component | undefined = components[currentId];
const node: ComponentGraphNode | undefined = graph[currentId];
Expand All @@ -80,60 +83,75 @@ 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;
case "if":
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))
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[] | number =
component.condition;
if (typeof cond === "string") {
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 (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;
}
}
}

return steps;
}
}
75 changes: 43 additions & 32 deletions app/routes/selectStage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useNavigate } from "react-router";
import { bubbles } from "~/bubbles";

const TOTAL_STAGES = 24;
const TOTAL_STAGES = 20;

const PATH_WIDTH = 1100;
const CENTER_X = PATH_WIDTH / 2;
Expand Down Expand Up @@ -103,58 +103,66 @@ const personDecorations = [
{
src: "/if_false.png",
alt: "If False Character",
top: "50rem",
right: "4rem",
stageIndex: 8,
width: 76,
offsetX: 50,
offsetY: -100,
},
{
src: "/if_true.png",
alt: "If True Character",
top: "47%",
left: "18rem",
stageIndex: 19,
width: 76,
offsetX: 40,
offsetY: -150,
},
{
src: "/pop_vanilla.png",
alt: "Pop Vanilla Character",
top: "37rem",
left: "30rem",
stageIndex: 0,
width: 64,
offsetX: 40,
offsetY: -80,
},
{
src: "/pop_strawberry.png",
alt: "Pop Strawberry Character",
top: "14rem",
right: "4rem",
stageIndex: 16,
width: 64,
offsetX: 40,
offsetY: -120,
},
{
src: "/pop_chocolate.png",
alt: "Pop Chocolate Character",
bottom: "27rem",
left: "35%",
stageIndex: 10,
width: 64,
offsetX: 40,
offsetY: -120,
},
{
src: "/push_vanilla.png",
alt: "Push Vanilla Character",
top: "25rem",
left: "22rem",
stageIndex: 13,
width: 72,
offsetX: 40,
offsetY: -120,
},
{
src: "/push_strawberry.png",
alt: "Push Strawberry Character",
bottom: "45rem",
left: "45%",
stageIndex: 3,
width: 72,
offsetX: 20,
offsetY: -150,
},
{
src: "/push_chocolate.png",
alt: "Push Chocolate Character",
bottom: "37rem",
right: "5rem",
stageIndex: 6,
width: 64,
offsetX: 40,
offsetY: -120,
},
];

Expand Down Expand Up @@ -182,21 +190,6 @@ export default function SelectStage() {
}
/>
))}
{personDecorations.map((decor, index) => (
<img
key={`person-decor-${index}`}
src={decor.src}
alt={decor.alt}
className="pointer-events-none absolute z-0 opacity-90"
style={{
width: decor.width,
top: decor.top,
left: decor.left,
right: decor.right,
bottom: decor.bottom,
}}
/>
))}

{/* Back button */}
<div className="relative z-10 mt-6 w-full px-4">
Expand Down Expand Up @@ -240,6 +233,24 @@ export default function SelectStage() {
/>
</svg>

{personDecorations.map((decor, index) => {
const pos = stagePositions[decor.stageIndex];
return (
<img
key={`person-decor-${index}`}
src={decor.src}
alt={decor.alt}
className="pointer-events-none absolute z-0 opacity-90"
style={{
position: "absolute",
width: decor.width,
left: pos.x + decor.offsetX,
top: pos.y + decor.offsetY,
}}
/>
);
})}

{stagePositions.map((pos, i) => {
const stageNum = i + 1;
return (
Expand Down
7 changes: 0 additions & 7 deletions app/routes/stage.$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,6 @@ function getComponentSrc(
) {
return { src: baseSrc, overlaySrc: `/cone_${condition}.png` };
}
// Flavor
if (
typeof condition === "string" &&
["vanilla", "chocolate", "strawberry"].includes(condition)
) {
return { src: baseSrc, overlaySrc: `/ice_${condition}.png` };
}
// Flavor[]
if (Array.isArray(condition)) {
const overlayImages = condition.map((flavor) => `/ice_${flavor}.png`);
Expand Down
Loading