Repository navigation
Card Service & Testing
The Card Library is the single source of static card information. It loads card definitions from configs/cards.json, validates them, and lets other teams retrieve them by ID through the CardService API.
A card definition describes what every copy of a card has in common: its name, energy cost, type, rarity, target, effects and artwork. It does not represent a card currently in a deck or hand.
Team 6 owns these definitions and the Card Library. Team 5 owns runtime cards, decks, hands, drawing, targeting, playing and discarding. Team 7 owns the player's energy, health, block/armour and player status effects.
configs/cards.json
-> CardConfigLoader loads and validates definitions
-> CardLibrary registers definitions by unique ID
-> CardService exposes read-only lookup operations
-> Team 5 creates and manages runtime cards
-> Team 1 or Team 7 applies the card effects
Create the library once during game or combat setup:
List<CardConfig> configs = CardConfigLoader.loadCards();
CardService cardService = new CardLibrary(configs);-
CardConfigLoader.loadCards()reads the defaultconfigs/cards.jsonasset. - The loader validates all required fields, effect values and durations, and rejects duplicate IDs.
-
CardLibraryregisters each valid definition under its unique ID. - Other systems receive the
CardServiceinterface instead of depending on the library's internal map.
There is currently no card-specific registration in ServiceLocator. Pass the same CardService instance to consumers through a constructor or setup method until a shared global lifecycle is agreed upon.
To retrieve the definition of the Strike card:
CardConfig strike = cardService.getCard("strike")
.orElseThrow(() -> new IllegalStateException("Card definition not found"));
int energyCost = strike.cost;
TargetType targetType = strike.target;
EffectConfig[] effects = strike.effects;- We request the card using its stable ID,
strike. -
getCard()returns anOptional<CardConfig>so a missing ID never silently producesnull. - Team 5 reads
costto check whether the player can play the card. - Team 5 reads
targetto determine how target selection should work. - Team 5 applies each item in
effectsin array order.
IDs are case-sensitive. Unknown, null, empty and whitespace-only IDs return Optional.empty().
Other systems should depend on com.csse3200.game.cards.CardService:
public interface CardService {
Optional<CardConfig> getCard(String cardId);
List<CardConfig> getAllCards();
}- Returns the definition registered under the exact, case-sensitive ID.
- Returns
Optional.empty()for an unknown,null, empty or whitespace-only ID. - Does not draw a card or change deck, hand or discard state.
- Returns an unmodifiable snapshot of all registered definitions.
- Does not expose the library's internal collection.
- Does not guarantee iteration order. Sort the result when a display order is required.
The returned list cannot be structurally modified, but the current libGDX configuration classes use public fields. Consumers must treat every returned CardConfig, EffectConfig and effects array as read-only.
Each object in the root cards array contains:
| Field | Java type | Meaning and validation |
|---|---|---|
id |
String |
Unique, non-blank, case-sensitive lookup key with no surrounding whitespace. |
name |
String |
Non-blank player-facing name. |
description |
String |
Required player-facing rules text. |
cost |
int |
Energy required to play; must be zero or greater. |
type |
CardType |
Broad card category, separate from the effects it performs. |
rarity |
Rarity |
Rarity used by reward and shop systems. |
target |
TargetType |
Target-selection mode that Team 5 must use. |
effects |
EffectConfig[] |
One or more effects, applied in array order. |
texturePath |
String |
Non-blank path relative to the core assets directory. |
| Type | Values |
|---|---|
CardType |
ATTACK, SKILL, POWER, STATUS, CURSE
|
Rarity |
COMMON, UNCOMMON, RARE
|
TargetType |
SELF, SINGLE_ENEMY, ALL_ENEMIES
|
EffectType |
DAMAGE, BLOCK, HEAL, POISON, VULNERABLE, FEEBLE, STRENGTH
|
Card type and card behaviour are deliberately separate. For example, a defence card is a SKILL with a BLOCK or HEAL effect. DEFENCE is not a CardType value.
Every effect has a type, a positive value and a duration:
| Effect | Meaning of value
|
duration rule |
|---|---|---|
DAMAGE |
Damage amount | Must be 0; applies immediately. |
BLOCK |
Block/armour amount | Must be 0; applies immediately. |
HEAL |
Health restored | Must be 0; applies immediately. |
POISON |
Effect magnitude or stacks | Must be a positive number of turns. |
VULNERABLE |
Effect magnitude or stacks | Must be a positive number of turns. |
FEEBLE |
Effect magnitude | Must be a positive number of turns. |
STRENGTH |
Strength amount | Must be 0; lasts for the rest of combat. |
In Team 6's schema, duration is always measured in turns. Team 7 and the enemy system store, decrement and resolve the statuses that they own.
Effects are performed in the order in which they appear. poison_dagger first deals damage and then applies Poison:
{
"id": "poison_dagger",
"name": "Poison Dagger",
"description": "Deal 4 damage. Apply 3 Poison for 3 turns.",
"cost": 1,
"type": "ATTACK",
"rarity": "UNCOMMON",
"target": "SINGLE_ENEMY",
"effects": [
{ "type": "DAMAGE", "value": 4, "duration": 0 },
{ "type": "POISON", "value": 3, "duration": 3 }
],
"texturePath": "images/cards/poison_dagger.png"
}The Team 6 tests cover the consumer contract at three levels:
-
CardLibraryTesttests registration, duplicate rejection, lookup behaviour and unmodifiable list snapshots. -
CardConfigLoaderTest.shouldIntegrateWithCardLibraryloads the asset format, constructs a library and retrieves a definition. -
CardsEndToEndTestloads the realconfigs/cards.json, validates and registers it, and checks all six Sprint 1 definitions.
Run the card tests from the source directory:
./gradlew core:test --tests "com.csse3200.game.cards.*"One definition can describe many copies of the same card. If the player has three Strike cards, the game needs three runtime cards but only one definition of Strike's name, artwork, cost and effects.
Keeping these responsibilities separate also prevents the Card Library from depending on Team 5's choice of runtime representation. Team 5 can change its deck implementation or switch between ECS and Scene2D without changing CardConfig or CardService.
Consumers need only two operations: retrieve one card or list the available cards. Depending on the small interface hides the library's map, prevents other systems from registering definitions accidentally, and makes consumer code easier to test with a stub service.
A card ID may be missing because of an invalid save file, outdated content or an integration mistake. Optional makes the missing case explicit and avoids returning an unexplained null. Known built-in IDs may use orElseThrow(), while IDs from saved or external data should handle the empty result gracefully.
| Decision | Agreed result | Teams affected |
|---|---|---|
| Static definitions versus runtime cards | Team 6 supplies reusable definitions; Team 5 owns runtime instances/entities. | 5, 6 |
| Retrieval boundary | Consumers use the two-method CardService interface. |
2, 5, 6 |
| Missing lookup | Unknown, null and blank IDs return Optional.empty(). |
2, 5, 6 |
| Card categories | Use ATTACK, SKILL, POWER, STATUS, CURSE; defence and utility are gameplay groupings. |
5, 6 |
| Target modes | Sprint 1 supports self, one enemy and all enemies. | 5, 6 |
| Multiple effects | Supported; consumers execute effects in array order. | 1, 5, 6, 7 |
| Energy representation |
cost is a non-negative integer; Team 7 owns live energy and Team 5 requests spending. |
5, 6, 7 |
| Timed effects |
POISON, VULNERABLE and FEEBLE require a positive duration measured in turns. |
1, 5, 6, 7 |
| Combat-long effect |
STRENGTH uses duration 0 and lasts for the combat. |
1, 5, 6, 7 |
| Service access | Explicit dependency passing is supported; ServiceLocator registration remains undecided. |
5, 6 |
| Listing order |
getAllCards() does not guarantee an order. |
2, 5, 6 |