This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Talishar is the PHP backend game engine for a browser-based Flesh and Blood card game platform. The frontend (Talishar-FE, TypeScript/React) lives in a sibling directory. The game state is file-based (not DB-driven), with Redis caching and MySQL for user accounts.
# Start/stop (Docker)
bash start.sh # Setup + docker compose up -d
bash stop.sh # docker compose down
# Lint
make lint # PHP 8.1 linting via Docker
# Tests (PHPUnit 10)
composer install --dev
./vendor/bin/phpunit # All tests
./vendor/bin/phpunit --testsuite "Security Tests" # Specific suite
./vendor/bin/phpunit --testsuite "Validation Tests"
./vendor/bin/phpunit --testsuite "Business Logic Tests"Ports: Web: 8080, PhpMyAdmin: 5001, Redis: 6382, Xdebug: 9003
Frontend → ProcessInput.php / ProcessInputAPI.php (validates input)
→ ParseGamestate.php (loads state from /Games/{gameName}/GameFile.txt)
→ GameLogic.php / CardLogic.php / Ability files (executes rules)
→ WriteGamestate.php (persists state to file)
→ GetNextTurn.php → BuildGameState.php (serializes JSON response to frontend)
- GetNextTurn.php — Main API endpoint returning game state JSON
- ProcessInput.php / ProcessInputAPI.php — Input processing and routing
- GameLogic.php — Core game mechanics and turn flow
- CoreLogic.php — Essential engine functions
- Constants.php — Zone definitions (HAND, BANISH, GRAVEYARD, PLAY, DECK), player IDs
- CardDictionary.php — Card data lookup (~211KB)
- CardLogic.php — Card ability implementations (~223KB)
- CombatChain.php — Combat resolution (~89KB)
CharacterAbilities.php, AuraAbilities.php, AllyAbilities.php, ItemAbilities.php, WeaponLogic.php, LandmarkAbilities.php, PermanentAbilities.php, CurrentEffectAbilities.php
- CardDictionaries/ — Card definitions by set (29 sets). Card stats, types, keywords.
- Classes/ — OOP architecture.
Card.phpis the base class. - Classes/CardObjects/ — Set-specific card implementations (e.g.,
HVYCards.php). This is where new cards go. - APIs/ — ~40 REST API endpoints for the frontend
- Libraries/ — Utility functions (validation, HTTP, caching, CSRF, networking)
- DecisionQueue/ — Async decision queue system (AwaitEffects.php, DecisionQueueEffects.php)
- AccountFiles/ — User auth (OAuth via Metafy/Patreon), signup, sessions
- Database/ — Schema and migrations
- GeneratedCode/CompiledCode/ — Auto-generated card code
New cards go in Classes/CardObjects/{SET}Cards.php — not scattered in switch statements.
Use zzCardCodeGenerator.php to auto-generate card stats from FabCube JSON. Then create a Card object with these key methods:
__construct— Sets$cardID,$controller. Optionally$baseCard(for color cycles) or$archetype(for shared patterns likewindup).PlayAbility— Resolution abilities (attack becomes attacking, non-attack resolves)IsPlayRestricted— Returns false when the card's own play conditions aren't metPayAdditionalCosts— Targeting and costs when card goes on stackCombatEffectActive— Returns true if a layer continuous effect should applyEffectPowerModifier— Power bonus from layer continuous effects
Methods to avoid unless specifically needed:
SpecialType()— Only needed when coding cards before FabCube database is updated. If the card exists in FabCube/generated dictionaries, do NOT add this method.ArcaneDamage()/ActionsThatDoArcaneDamage()— Only for cards whose resolution abilities deal arcane damage (so Blaze can detect it). Do NOT use for cards that deal arcane viaProcessAbility(ability mode).
To hook new methods into the engine, add above the relevant switch statement:
$card = GetClass($card, $player);
if ($card != "-") $card->Method();The Decision Queue handles async player decisions (targeting, choices). DQs are asynchronous — code after DQ blocks runs before the DQs execute.
Await is the preferred wrapper around DQs. Uses named variables ($dqVars) instead of tracking $lastResult:
Await($player, "DeckTopCards", "cardIDs", number:3, subsequent:false);
Await($player, "RevealCards");
Await($player, $this->cardID, mode:"choose_cards");
Await($player, "ShuffleDeck", final:true); // final:true clears $dqVarsKey DQ commands: MULTIZONEINDICES, CHOOSEMULTIZONE, SETDQCONTEXT, MZREMOVE, SETLAYERTARGET, ELSE, SPECIFICCARD, PASSPARAMETER
Cards are referenced as {ZONE}-{index}, relative to a player: MYCHAR-0 = player's hero, THEIRCHAR-0 = opponent's hero. Convert between index and unique ID with CleanTarget / CleanTargetToIndex.
Search syntax (for SearchMultizone / MULTIZONEINDICES):
"MYDISCARD:cost<2;type=AA&THEIRDISCARD:cost<2;type=AA"
Zone, then : for conditions, ; separates AND conditions, & combines searches across zones.
Zones: CHAR, ALLY, ARS/ARSENAL, AURAS, BANISH, ITEMS, LAYER, HAND, DISCARD, PERM, PITCH, DECK, SOUL, LANDMARK, CC/COMBATCHAINLINK, PASTCHAINLINKS, COMBATCHAINATTACKS, CURRENTTURNEFFECTS
To track per-turn game events (e.g., "if you've destroyed X this turn"), use ClassState variables:
Constants.php— Define constant (next sequential number after$CS_NumWeaponsActivated = 115), add toglobaldeclaration inResetMainClassState(), and initialize to 0 in the same function.MenuFiles/StartHelper.php— Append0to the class state string on line 35 (one value per constant).- Trigger location — Call
IncrementClassState($player, $CS_YourConstant)where the event occurs (e.g.,AuraAbilities.php:DestroyAura()for aura destruction). - Check —
GetClassState($player, $CS_YourConstant) > 0in card logic.
Existing examples: $CS_NumSeismicSurgeDestroyed, $CS_NumRedPlayed, $CS_NumWeaponsActivated.
For "choose 1" effects on attack cards, use BUTTONINPUT with Await, then resolve via a trigger layer:
// In PlayAbility — queue the choice
AddDecisionQueue("SETDQCONTEXT", $player, "Choose a mode for " . CardLink($cardID));
AddDecisionQueue("BUTTONINPUT", $player, "Option_A,Option_B,Option_C");
AddDecisionQueue("SHOWMODES", $player, $cardID, 1);
Await($player, $cardID, final:true);
// In SpecificLogic — log choice and create trigger layer
global $dqVars;
WriteLog(CardLink($this->cardID) . " mode: " . GamestateUnsanitize($dqVars["LASTRESULT"]));
AddLayer("TRIGGER", $this->controller, $this->cardID, additionalCosts:$dqVars["LASTRESULT"]);
// In ProcessTrigger — resolve the actual effect
function ProcessTrigger($uniqueID, $target = "-", $additionalCosts = "-", $from = "-") {
switch ($additionalCosts) {
case "Option_A": /* effect */ break;
case "Option_B": /* effect */ break;
}
}Key points:
- Button labels use underscores as spaces. Use
GamestateUnsanitize()(notstr_replace) to convert back for display. SHOWMODESlogs the player's choice. Usefinal:truesince no further DQ vars are needed.- Do NOT resolve effects directly in
SpecificLogic— always create a trigger layer so the effect goes on the stack properly. - The engine routes trigger resolution through
CardLogic.php→ProcessTrigger()whenadditionalCostsis not a known keyword like "ATTACKTRIGGER".
When a card creates multiple possible effects (e.g., modal choices for +power or go again), use suffixed card IDs to distinguish them:
// In SpecificLogic — create effect with suffix
AddCurrentTurnEffect($this->cardID . "-BUFF", $this->controller);
AddCurrentTurnEffect($this->cardID . "-GOAGAIN", $this->controller);The engine strips the suffix and routes to the card class, passing the suffix as $parameter/$param. Implement methods that check the suffix:
function CombatEffectActive($parameter = '-', $defendingCard = '', $flicked = false) {
return $parameter == "BUFF" || $parameter == "GOAGAIN";
}
function EffectPowerModifier($param, $attached = false) {
if ($param == "BUFF") return 2;
return 0;
}
function CurrentEffectGrantsGoAgain($param) {
return $param == "GOAGAIN";
}Key points:
CombatEffectActiveties the effect to the current attack (cleans up when chain link closes)- Do NOT use
GiveAttackGoAgain()directly — useCurrentEffectGrantsGoAgainvia the effect system instead - Do NOT return
trueunconditionally fromCombatEffectActive— always check$parameter
CardLink($cardID)— Single argument is sufficient; the second argument (display name) defaults to the same cardID if omitted.GamestateUnsanitize($str)— Converts underscored gamestate strings back to human-readable form (replaces_with spaces). Prefer this over manualstr_replace("_", " ", ...).WriteLog($msg)— Writes to the game log visible to players.
- Card IDs: lowercase with underscores (e.g.,
fyendals_spring_tunic,command_and_conquer_red) - Player-relative zones:
MY/THEIRprefix - State variables:
$currentPlayer(who has priority, 1 or 2),$turn[0](phase: M=Main, A=Action, D=Defense) - Session handling: Session lock is released immediately after capturing data to prevent deadlock
- Container path mapping: Project root maps to
/var/www/html/gamein Docker