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
12 changes: 12 additions & 0 deletions src/daily/__tests__/schedule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,15 @@ test("National Egg Day builds a 27-card deck of Fried Egg cards", () => {
expect(deck.features.length).toEqual(3);
expect(Object.keys(deck.cards).length).toEqual(27);
});

test("National Ice Cream Day is scheduled for July 18th and recurs annually", () => {
expect(getPuzzleForDate("2026-07-18").name).toEqual("National Ice Cream Day");
expect(getPuzzleForDate("2027-07-18").name).toEqual("National Ice Cream Day");
expect(getPuzzleForDate("2025-07-18").name).not.toEqual("National Ice Cream Day");
});

test("National Ice Cream Day builds a 27-card deck of Sundae Glass cards", () => {
const deck = DAILY_PUZZLE_SCHEDULE["2026-07-18"].createDeck();
expect(deck.features.length).toEqual(3);
expect(Object.keys(deck.cards).length).toEqual(27);
});
13 changes: 13 additions & 0 deletions src/daily/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ export const DAILY_PUZZLE_SCHEDULE: DailyPuzzleSchedule = {
{ idPrefix: "daily-egg" }
),
},
"2026-07-18": {
name: "National Ice Cream Day",
createDeck: () =>
new GeometricDeckGenerator(
{
numbers: [1, 2, 3],
scoops: [1, 2, 3],
colors: ["Pink", "Brown", "Mint"],
},
{ shapes: "Ice Cream - Sundae Glass", topping: "Cherry", sauce: "Chocolate" },
{ idPrefix: "daily-icecream" }
),
},
};

/** The deck used whenever no scheduled puzzle matches the date. */
Expand Down
39 changes: 38 additions & 1 deletion src/deckBuilder/CardSvg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,33 @@ const resolveYolks = (
return supported.includes(yolks as 1 | 2 | 3) ? yolks : supported[0];
};

const resolveScoops = (
supported: ShapeFeatureSupport["scoops"],
scoops: number
): number => {
if (supported === false || supported === undefined) return 1;
if (supported === true) return scoops;
return supported.includes(scoops as 0 | 1 | 2 | 3 | 4) ? scoops : supported[0];
};

const resolveTopping = (
supported: ShapeFeatureSupport["topping"],
topping: CardData["topping"]
): CardData["topping"] => {
if (supported === false || supported === undefined) return "None";
if (supported === true) return topping;
return supported.includes(topping) ? topping : "None";
};

const resolveSauce = (
supported: ShapeFeatureSupport["sauce"],
sauce: CardData["sauce"]
): CardData["sauce"] => {
if (supported === false || supported === undefined) return "None";
if (supported === true) return sauce;
return supported.includes(sauce) ? sauce : "None";
};

interface Props {
card: CardData;
/** Document-unique id, used to namespace this card's SVG defs. */
Expand All @@ -73,6 +100,9 @@ export const CardSvg = ({ card, cardId }: Props) => {
const filterName: FilterName = supports.filters === false ? "none" : card.filters;
const rotation = resolveRotation(supports.rotations, card.rotations);
const yolks = resolveYolks(supports.yolks, card.yolks);
const scoops = resolveScoops(supports.scoops, card.scoops);
const topping = resolveTopping(supports.topping, card.topping);
const sauce = resolveSauce(supports.sauce, card.sauce);
let patternName: PatternName = supports.patterns === false ? "solid" : card.patterns;
if (PATTERN_DEFS[patternName].colorsUsed > colorCount) {
patternName = "solid";
Expand All @@ -98,7 +128,14 @@ export const CardSvg = ({ card, cardId }: Props) => {
rotation ? `rotate(${rotation}, ${rotationCenter.x}, ${rotationCenter.y})` : undefined
}
>
<shape.Component colors={colors} fill={paint.fill} yolks={yolks as 1 | 2 | 3} />
<shape.Component
colors={colors}
fill={paint.fill}
yolks={yolks as 1 | 2 | 3}
scoops={scoops as 0 | 1 | 2 | 3 | 4}
topping={topping}
sauce={sauce}
/>
</g>
</svg>
);
Expand Down
3 changes: 3 additions & 0 deletions src/deckBuilder/__tests__/cardSvg.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const CARD: CardData = {
filters: "shadow",
patterns: "striped",
yolks: 1,
scoops: 1,
topping: "None",
sauce: "None",
};

const renderCard = (card: CardData) =>
Expand Down
4 changes: 2 additions & 2 deletions src/deckBuilder/__tests__/deckRules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const DECK: GeneratedDeckMetaData = {
numbers: [9, 3, 4],
};

test("yolks is the only shape-only feature today", () => {
expect(SHAPE_ONLY_FEATURES).toEqual(["yolks"]);
test("yolks, scoops, topping, and sauce are the shape-only features today", () => {
expect(SHAPE_ONLY_FEATURES).toEqual(["yolks", "scoops", "topping", "sauce"]);
});

test("a shape owning no internal feature backs a single card", () => {
Expand Down
5 changes: 5 additions & 0 deletions src/deckBuilder/__tests__/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ const LEGACY_SHAPE_NAMES = [
"Tracks - Wolf",
"Tracks - Frog",
"Fried Egg",
"Ice Cream - Waffle Cone",
"Ice Cream - Sugar Cone",
"Ice Cream - Sundae Cup",
"Ice Cream - Sundae Glass",
"Ice Cream - Banana Split",
];

test("shape registry keeps the legacy shape names", () => {
Expand Down
18 changes: 18 additions & 0 deletions src/deckBuilder/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { PATTERN_NAMES, PatternName } from "./patterns";
export const NUMBERS = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const;
export const ROTATIONS: readonly Rotation[] = [0, 90, 180, 270];
export const YOLKS = [1, 2, 3] as const;
export const SCOOPS = [0, 1, 2, 3, 4] as const;
export const TOPPINGS = [
"Chocolate Chips",
"Cherry",
"Rainbow Sprinkles",
"Twisty Pretzel",
"None",
] as const;
export const SAUCES = ["Chocolate", "Strawberry", "Toffee", "Pistachio", "None"] as const;

/**
* The features a generated deck can vary, mapped to their option value type.
Expand All @@ -22,6 +31,9 @@ interface FeatureOptionMap {
filters: FilterName;
patterns: PatternName;
yolks: (typeof YOLKS)[number];
scoops: (typeof SCOOPS)[number];
topping: (typeof TOPPINGS)[number];
sauce: (typeof SAUCES)[number];
}

export type FeatureName = keyof FeatureOptionMap;
Expand Down Expand Up @@ -57,6 +69,9 @@ export const FEATURES: { readonly [F in FeatureName]: FeatureConfig<F> } = {
filters: { label: "Filter", options: FILTER_NAMES },
patterns: { label: "Pattern", options: PATTERN_NAMES },
yolks: { label: "Yolks", options: YOLKS, requiresShapeSupport: true },
scoops: { label: "Scoops", options: SCOOPS, requiresShapeSupport: true },
topping: { label: "Topping", options: TOPPINGS, requiresShapeSupport: true },
sauce: { label: "Sauce", options: SAUCES, requiresShapeSupport: true },
};

export const FEATURE_NAMES = Object.keys(FEATURES) as FeatureName[];
Expand All @@ -77,6 +92,9 @@ export const DEFAULT_CARD: CardData = {
filters: "none",
patterns: "solid",
yolks: 1,
scoops: 2,
topping: "None",
sauce: "None",
};

export function getFeatureOptions<F extends FeatureName>(feature: F): readonly FeatureValue<F>[] {
Expand Down
Loading
Loading