Proposed
Building on ADR-016 (Plugin Architecture) and the research in R-009 (Mechanic-Application Integration), we need to decide how much logic should live in mechanics versus core Itemdeck.
Itemdeck is a JSON viewer. It displays information from JSON files with configurable presentation. Gaming mechanics should extend this by:
- Configuring existing behaviour differently (primary)
- Adding minimal custom logic (secondary, only when necessary)
| Behaviour | Exists in Core? | Mechanic Provides |
|---|---|---|
| Cards start face-down | ✅ defaultCardFace: 'back' |
Setting override |
| Max 2 cards visible | ✅ maxVisibleCards: 2 |
Setting override |
| Cards shuffled | ✅ shuffleOnLoad: true |
Setting override |
| Card duplication | ❌ Not in core | Data transformer |
| Match detection | ❌ Not in core | Custom logic |
| Score tracking | ❌ Not in core | Custom state |
| Win condition | ❌ Not in core | Custom logic |
Key Insight: 3 out of 7 behaviours are just setting overrides. Only 4 require custom code, and 3 of those (match detection, score, win) are related game state.
Adopt a Configuration-First Mechanic Architecture where:
- Mechanics are primarily configuration files that override settings
- Data transformation is a declarative config where possible
- Custom logic is minimal - only game-specific rules
- Core Itemdeck expands to support mechanic needs through general features
interface MechanicDefinition {
// Metadata
manifest: MechanicManifest;
// LAYER 1: Settings Configuration (no code needed)
settingsOverrides: {
// Values to force while mechanic is active
values: Partial<SettingsState>;
// Settings to lock (user can't change)
locked: (keyof SettingsState)[];
// Settings to hide from UI
hidden: (keyof SettingsState)[];
};
// LAYER 2: Data Transformation Config
dataTransform?: {
// Card duplication (memory game)
duplicate?: {
enabled: true;
suffixes: ['a', 'b']; // Creates {id}-a, {id}-b
pairIdField: '_pairId'; // Metadata field for matching
};
// Subset selection
subset?: {
count: number | 'all';
method: 'random' | 'first' | 'last';
};
// Force shuffle after transform
shuffleAfterTransform?: boolean;
};
// LAYER 3: Custom Logic (only when configuration isn't enough)
customLogic?: {
// State store factory
createStore: () => MechanicStore;
// Interaction handler
interactionHandler?: MechanicInteractionHandler;
// Win/end condition
endCondition?: (state: MechanicState) => boolean;
};
// LAYER 4: UI Components
components: {
ActivePanel: React.ComponentType; // Control panel
GridOverlay?: React.ComponentType; // Score, timer, etc.
CardDecorator?: React.ComponentType<{ cardId: string }>;
};
}// src/mechanics/memory/definition.ts
export const memoryMechanicDefinition: MechanicDefinition = {
manifest: {
id: 'memory',
name: 'Memory Game',
description: 'Match pairs of cards',
icon: MemoryIcon,
version: '1.0.0',
},
// LAYER 1: Just configuration - no code
settingsOverrides: {
values: {
defaultCardFace: 'back', // All cards start face-down
maxVisibleCards: 2, // Only 2 cards visible at once
shuffleOnLoad: true, // Always shuffle
dragModeEnabled: false, // Disable drag in memory mode
},
locked: [
'defaultCardFace',
'maxVisibleCards',
'shuffleOnLoad',
'dragModeEnabled',
],
hidden: [
'dragFace', // Irrelevant when drag disabled
],
},
// LAYER 2: Declarative data transformation
dataTransform: {
duplicate: {
enabled: true,
suffixes: ['a', 'b'],
pairIdField: '_pairId',
},
subset: {
count: 8, // Default: 8 pairs = 16 cards
method: 'random',
},
shuffleAfterTransform: true,
},
// LAYER 3: Only match logic is truly custom
customLogic: {
createStore: () => createMemoryStore(),
interactionHandler: memoryInteractionHandler,
endCondition: (state) => state.matchedPairs === state.totalPairs,
},
// LAYER 4: UI
components: {
ActivePanel: MemoryControlPanel,
GridOverlay: MemoryScoreOverlay,
},
};Core should support these features (some already exist):
| Feature | Status | Used By |
|---|---|---|
defaultCardFace setting |
✅ Exists | Memory (force back) |
maxVisibleCards setting |
✅ Exists | Memory (force 2) |
shuffleOnLoad setting |
✅ Exists | Memory (force on) |
| Settings override API | 🆕 Needed | All mechanics |
| Card duplication transform | 🆕 Needed | Memory |
| Subset/sampling | ✅ Exists (randomSelectionCount) |
Memory |
| Mechanic-aware click handling | 🆕 Needed | Memory, Quiz |
| Grid overlay slot | 🆕 Needed | All mechanics |
- Most mechanic behaviour is zero-code - just JSON/config
- Core Itemdeck gains features that benefit non-gaming use too
- Easy to create new mechanics - copy config, tweak values
- Clear separation - game logic is small and isolated
- Easier testing - test config separately from logic
- Less maintenance - fewer custom code paths
Add to Core Itemdeck when:
- Feature is generally useful (not game-specific)
- Feature is about display/configuration
- Feature can be expressed as a setting
- Multiple mechanics would benefit
Add to Mechanic when:
- Logic is game-specific (scoring, win conditions)
- Logic requires game state tracking
- Logic involves timing/sequences
Question: Should "cards match if they share an attribute" be a core feature?
Analysis:
- ✅ Could be useful for non-gaming (highlight related cards)
- ✅ Multiple mechanics could use (Memory, Quiz)
- ❌ Matching rules vary per mechanic
- ❌ Game-specific (what counts as "match")
Decision: Keep matching logic in mechanics. Core provides metadata (pairId), mechanic decides match rules.
- Smaller mechanic codebase - mostly configuration
- Better factored core - features split out properly
- Easier mechanic development - less to implement
- Reusable patterns - same transform works across mechanics
- Declarative - configuration is easier to understand than code
- Core grows - more settings, more code paths
- Indirection - config interpreted at runtime
- Limited flexibility - unusual mechanics might not fit pattern
- Migration - existing mechanic code needs refactoring
- Escape hatch -
customLogiclayer for anything config can't handle - Feature flags - new core features behind flags during development
- Gradual migration - don't refactor all mechanics at once
- Add settings override API
- Add data transformer pipeline
- Add grid overlay slot
- Add mechanic-aware click handling
- Convert Memory to configuration-first
- Extract reusable transforms (duplicate, subset)
- Minimal custom logic (match detection, scoring)
- Collection - mostly config (owned/wishlist toggles)
- Quiz - config + question logic
- Snap Ranking - config + rating logic
- ADR-016: Gaming Mechanics Plugin Architecture
- R-009: Mechanic-Application Integration
- R-005: Gaming Mechanics State Patterns
Applies to: Itemdeck v0.11.0+