Game Mechanics
Understand the technical implementation behind Crafter LoL’s question generation, answer validation, and scoring system.
Question Generation
The backend generates questions dynamically by selecting random craftable items from the League of Legends item database.
Selection Process
Random Item Selection
The game service selects a random craftable item as the target: // From GameService.java:42-43
List < Item > itemList = new ArrayList <>( craftableItems . values ());
Item targetItem = itemList . get ( random . nextInt ( itemList . size ()));
Only items that have crafting components (items with a “from” list) are eligible as targets.
Extract Correct Components
The system retrieves the item’s actual crafting recipe: // From GameService.java:48-66
List < String > correctComponentIds = targetItem . getFrom ();
Map < String , Item > allItems = itemService . getAllItems ();
List < ItemOption > correctComponents = correctComponentIds . stream ()
. map (allItems :: get)
. filter (Objects :: nonNull)
. map (item -> ItemOption . builder ()
. itemId ( item . getId ())
. name ( item . getName ())
. imageUrl ( item . getImageUrl ())
. cost ( item . getTotalCost ())
. build ())
. collect ( Collectors . toList ());
Each component includes:
Unique item ID
Display name
Image URL (from Riot’s CDN)
Total gold cost
Generate Distractor Items
The system adds incorrect items to create the challenge: // From GameService.java:69-74
int totalOptions = getTotalOptions (difficulty);
List < ItemOption > options = generateOptions (
correctComponentIds,
totalOptions,
difficulty
);
The number of options depends on difficulty:
Easy : 6 total options
Medium : 10 total options
Hard : 14 total options
Return Complete Question
The backend returns a structured question object: // From GameService.java:76-86
return GameQuestion . builder ()
. targetItemId ( targetItem . getId ())
. targetItemName ( targetItem . getName ())
. targetItemImageUrl ( targetItem . getImageUrl ())
. correctComponentIds (correctComponentIds)
. correctComponents (correctComponents)
. options (options)
. timeLimit ( getTimeLimit (difficulty))
. difficulty (difficulty)
. build ();
Distractor Generation
The most sophisticated part of the game mechanics is how distractor items are chosen, especially on Hard difficulty.
Easy and Medium Difficulty
On Easy and Medium modes, distractors are randomly selected from the item pool:
// From GameService.java:217-227
List < Item > fallback = allItems . values (). stream ()
. filter (item -> ! addedIds . contains ( item . getId ()))
. filter (item -> item . getTotalCost () > 0 )
. collect ( Collectors . toList ());
Collections . shuffle (fallback);
for ( Item d : fallback) {
if ( options . size () >= totalOptions) break ;
options . add ( toOption (d));
addedIds . add ( d . getId ());
}
Hard Difficulty - Smart Distractors
Hard mode uses an intelligent algorithm to select distractors that are similar to the correct components:
Phase 1: Tag-Based
Phase 2: Cost-Based
Phase 3: Fallback
First, the system collects tags from correct components: // From GameService.java:170-174
Set < String > correctTags = correctComponentIds . stream ()
. map (allItems :: get)
. filter (Objects :: nonNull)
. flatMap (item -> item . getTags () != null ?
item . getTags (). stream () : Stream . empty ())
. collect ( Collectors . toSet ());
Then selects items sharing tags AND similar cost: // From GameService.java:186-193
List < Item > smartDistractors = allItems . values (). stream ()
. filter (item -> ! addedIds . contains ( item . getId ()))
. filter (item -> item . getTotalCost () >= costMin &&
item . getTotalCost () <= costMax)
. filter (item -> item . getTags () != null &&
! Collections . disjoint ( item . getTags (), correctTags))
. collect ( Collectors . toList ());
Tags include categories like “Damage”, “CriticalStrike”, “SpellDamage”, “Armor”, etc.
If not enough tag-based distractors are found, add items with similar costs: // From GameService.java:176-184
int avgCost = ( int ) correctComponentIds . stream ()
. map (allItems :: get)
. filter (Objects :: nonNull)
. mapToInt (Item :: getTotalCost)
. average ()
. orElse ( 1000 );
int costMin = ( int ) (avgCost * 0.5 ); // 50% of average
int costMax = ( int ) (avgCost * 2.0 ); // 200% of average
// From GameService.java:202-212
List < Item > costDistractors = allItems . values (). stream ()
. filter (item -> ! addedIds . contains ( item . getId ()))
. filter (item -> item . getTotalCost () >= costMin &&
item . getTotalCost () <= costMax)
. collect ( Collectors . toList ());
If still not enough, add random items as final fallback: // From GameService.java:217-228
if ( options . size () < totalOptions) {
List < Item > fallback = allItems . values (). stream ()
. filter (item -> ! addedIds . contains ( item . getId ()))
. filter (item -> item . getTotalCost () > 0 )
. collect ( Collectors . toList ());
Collections . shuffle (fallback);
// Add until we reach totalOptions
}
On Hard difficulty, distractors are specifically chosen to trick you! They’ll have similar attributes to the correct answer.
Answer Validation
When you submit an answer, the backend validates your selection:
Receive Answer
Frontend sends selected component IDs: // From gameService.js:73-77
async validateAnswer ( targetItemId , selectedComponentIds ){
const response = await api . post ( API_CONFIG . endpoints . validate , {
targetItemId , selectedComponentIds
});
}
Validate Combination
Backend checks if the components match: // From GameService.java:89-92
boolean isCorrect = itemService . isValidCraftingCombination (
request . getTargetItemId (),
request . getSelectedComponentIds ()
);
The validation checks:
Do the selected IDs match the target item’s “from” list?
Are they in the correct quantity (some items need duplicates)?
Is the combination exactly right (no extra or missing items)?
Calculate Score
Points are calculated based on component count: // From GameService.java:259-271
private int calculateScore ( int componentCount) {
int baseScore = componentCount * 50 ;
// Bonus for complexity
if (componentCount >= 3 ) {
baseScore += 100 ;
} else if (componentCount == 2 ) {
baseScore += 50 ;
}
return baseScore;
}
While the backend calculates dynamic scores, the frontend currently uses fixed values (100 correct / 50 incorrect) from GAME_CONFIG.
Return Result
Backend sends back the validation result: // From GameService.java:133-141
return ValidationResponse . builder ()
. correct (isCorrect)
. correctComponentIds (correctIds)
. correctComponentNames (correctNames)
. correctComponents (correctComponents)
. incorrectComponentIds (incorrectIds)
. message (message)
. scorePoints (scorePoints)
. build ();
Frontend Data Adaptation
The frontend adapts backend responses to its internal data structure:
// From gameService.js:12-52
function adaptBackendResponse ( backendData ) {
// Transform targetItem
const targetItem = {
id: backendData . targetItemId ,
name: backendData . targetItemName ,
imageUrl: backendData . targetItemImageUrl ,
};
// Transform options - change itemId to id
const options = backendData . options . map ( item => ({
id: item . itemId ,
name: item . name ,
imageUrl: item . imageUrl ,
cost: item . cost ,
}));
// Transform correctComponents
const correctComponents = backendData . correctComponents ?. map ( item => ({
id: item . itemId || item . id ,
name: item . name ,
imageUrl: item . imageUrl ,
})) || [];
return {
targetItem ,
options ,
correctComponents ,
correctComponentIds: backendData . correctComponentIds ,
timeLimit: backendData . timeLimit ,
difficulty: backendData . difficulty ,
};
}
This adapter pattern allows the frontend and backend to evolve independently while maintaining compatibility.
Timer Mechanics
The timer has special behavior to improve user experience:
// From App.jsx:50-66
useEffect (() => {
if ( ! gameData || feedback || timeLeft <= 0 ) return ;
if ( selectedItems . length >= requiredSlots ) return ; // PAUSE when slots full
const timer = setInterval (() => {
setTimeLeft (( prev ) => {
if ( prev <= 1 ) {
handleTimeout ();
return 0 ;
}
return prev - 1 ;
});
}, 1000 );
return () => clearInterval ( timer );
}, [ gameData , feedback , timeLeft , selectedItems . length , requiredSlots ]);
Timer States :
Running : Counting down normally
Paused : All slots filled (gives you time to review)
Stopped : Feedback shown or time expired
Reset : New question loaded
The auto-pause when slots are full prevents accidental timeouts and lets you think about your selection!
State Management
The game uses React hooks for state management:
// From App.jsx:15-22
const [ gameData , setGameData ] = useState ( null );
const [ selectedItems , setSelectedItems ] = useState ([]);
const [ score , setScore ] = useState ( storageService . getScore ());
const [ bestScore , setBestScore ] = useState ( storageService . getBestScore ());
const [ timeLeft , setTimeLeft ] = useState ( GAME_CONFIG . timePerQuestion );
const [ feedback , setFeedback ] = useState ( null );
const [ isLoading , setIsLoading ] = useState ( true );
const [ error , setError ] = useState ( null );
Difficulty Levels Learn about Easy, Medium, and Hard mode differences
Scoring System Understand how points are calculated and tracked