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
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,7 @@ Error responses follow JSON-RPC 2.0 format:
"id": 1
}
```

**Gotcha — held tags vs blind tag offers:** `GameState.tags` is the list of skip rewards currently held (`G.GAME.tags`). Blind *offers* for the next skip remain under `blinds.small|big.tag_name` / `tag_effect`. Immediate tags may resolve on skip and leave `tags` empty.

**Gotcha — `economy.shop_slots` vs Area `shop`:** `economy.shop_slots` is how many cards the main shop holds (Overstock). `shop` is still the Area of cards currently for sale.
20 changes: 20 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,19 @@ The complete game state returned by most methods.
"stake": "WHITE",
"seed": "ABC123",
"won": false,
"tags": [],
"economy": {
"interest_cap": 25,
"interest_amount": 1,
"bankrupt_at": 0,
"discount_percent": 0,
"shop_slots": 2
},
"probabilities": { "normal": 1 },
"pool_flags": {},
"skips": 0,
"starting_deck_size": 52,
"consumable_usage_total": { "tarot": 1, "planet": 0, "spectral": 0, "tarot_planet": 1, "all": 1 },
"used_vouchers": {},
"hands": { ... },
"round": { ... },
Expand All @@ -712,6 +725,8 @@ The complete game state returned by most methods.
}
```

`tags` are held skip rewards (not the Small/Big blind *offer* tags under `blinds.*.tag_name`). `economy.shop_slots` is the shop card-slot count; `shop` remains the shop card Area. `consumable_usage_total` is omitted until a consumable has been used. `round` also includes `idol_card` / `mail_card` / `ancient_card` / `castle_card` targets and `free_rerolls`.

### Area

Represents a card area (hand, jokers, consumables, shop, etc.).
Expand Down Expand Up @@ -763,6 +778,11 @@ Represents a card area (hand, jokers, consumables, shop, etc.).
```json
{
"hands_left": 4,
"free_rerolls": 0,
"idol_card": { "suit": "S", "rank": "A" },
"mail_card": { "rank": "A" },
"ancient_card": { "suit": "S" },
"castle_card": { "suit": "S" },
"hands_played": 0,
"discards_left": 3,
"discards_used": 0,
Expand Down
133 changes: 123 additions & 10 deletions src/lua/utils/gamestate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -441,33 +441,63 @@ local function extract_round_info()
return {}
end

local current = G.GAME.current_round
local round = {}

if G.GAME.current_round.hands_left then
round.hands_left = G.GAME.current_round.hands_left
if current.hands_left ~= nil then
round.hands_left = current.hands_left
end

if G.GAME.current_round.hands_played then
round.hands_played = G.GAME.current_round.hands_played
if current.hands_played ~= nil then
round.hands_played = current.hands_played
end

if G.GAME.current_round.discards_left then
round.discards_left = G.GAME.current_round.discards_left
if current.discards_left ~= nil then
round.discards_left = current.discards_left
end

if G.GAME.current_round.discards_used then
round.discards_used = G.GAME.current_round.discards_used
if current.discards_used ~= nil then
round.discards_used = current.discards_used
end

if G.GAME.current_round.reroll_cost then
round.reroll_cost = G.GAME.current_round.reroll_cost
if current.reroll_cost ~= nil then
round.reroll_cost = current.reroll_cost
end

if current.free_rerolls ~= nil then
round.free_rerolls = current.free_rerolls
end

-- Chips is stored in G.GAME not G.GAME.current_round
if G.GAME.chips then
round.chips = G.GAME.chips
end

if current.idol_card then
round.idol_card = {
suit = convert_suit_to_enum(current.idol_card.suit),
rank = convert_rank_to_enum(current.idol_card.rank),
}
end

if current.mail_card then
round.mail_card = {
rank = convert_rank_to_enum(current.mail_card.rank),
}
end

if current.ancient_card then
round.ancient_card = {
suit = convert_suit_to_enum(current.ancient_card.suit),
}
end

if current.castle_card then
round.castle_card = {
suit = convert_suit_to_enum(current.castle_card.suit),
}
end

return round
end

Expand Down Expand Up @@ -570,6 +600,61 @@ local function get_tag_info(tag_key)
return result
end

---Extracts held tags from G.GAME.tags (skip rewards that have not resolved yet)
---@return GameState.Tag[] tags
local function extract_tags()
local tags = {}
if not G or not G.GAME or not G.GAME.tags then
return tags
end

for i, tag in ipairs(G.GAME.tags) do
local key = tag.key or ""
local info = get_tag_info(key)
tags[i] = {
key = key,
name = info.name,
effect = info.effect,
}
end

return tags
end

---Extracts economy helpers (interest, discount, shop slots)
---@return GameState.Economy economy
local function extract_economy()
local shop_slots = 2
if G and G.GAME and G.GAME.shop and G.GAME.shop.joker_max ~= nil then
shop_slots = G.GAME.shop.joker_max
end

return {
interest_cap = (G and G.GAME and G.GAME.interest_cap) or 25,
interest_amount = (G and G.GAME and G.GAME.interest_amount) or 1,
bankrupt_at = (G and G.GAME and G.GAME.bankrupt_at) or 0,
discount_percent = (G and G.GAME and G.GAME.discount_percent) or 0,
shop_slots = shop_slots,
}
end

---Extracts consumable usage totals (Fortune Teller / Satellite counters)
---@return table<string, integer>? usage
local function extract_consumable_usage_total()
if not G or not G.GAME or not G.GAME.consumeable_usage_total then
return nil
end

local usage = G.GAME.consumeable_usage_total
return {
tarot = usage.tarot or 0,
planet = usage.planet or 0,
spectral = usage.spectral or 0,
tarot_planet = usage.tarot_planet or 0,
all = usage.all or 0,
}
end

---Converts game blind status to uppercase enum
---@param status string Game status (e.g., "Defeated", "Current", "Select")
---@return string uppercase_status Uppercase status enum (e.g., "DEFEATED", "CURRENT", "SELECT")
Expand Down Expand Up @@ -754,6 +839,34 @@ function gamestate.get_gamestate()
state_data.seed = G.GAME.pseudorandom.seed
end

-- Held tags (skip rewards not yet resolved)
state_data.tags = extract_tags()

-- Economy helpers (interest / discount / shop slots)
state_data.economy = extract_economy()

-- Odds multiplier (Oops All 6s scales probabilities.normal)
state_data.probabilities = {
normal = (G.GAME.probabilities and G.GAME.probabilities.normal) or 1,
}

-- Pool flags (e.g. Gros Michel extinct)
state_data.pool_flags = G.GAME.pool_flags or {}

-- Blind skips this run (Throwback)
state_data.skips = G.GAME.skips or 0

-- Starting deck size (Erosion)
if G.GAME.starting_deck_size ~= nil then
state_data.starting_deck_size = G.GAME.starting_deck_size
end

-- Consumable usage totals (omitted until a consumable has been used)
local consumable_usage_total = extract_consumable_usage_total()
if consumable_usage_total then
state_data.consumable_usage_total = consumable_usage_total
end

-- Used vouchers (table<string, string>)
if G.GAME.used_vouchers then
local used_vouchers = {}
Expand Down
160 changes: 160 additions & 0 deletions src/lua/utils/openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,46 @@
"type": "boolean",
"description": "Whether the game has been won"
},
"tags": {
"type": "array",
"description": "Held tags from skips (not blind skip offers). Empty when none.",
"items": {
"$ref": "#/components/schemas/GameTag"
}
},
"economy": {
"$ref": "#/components/schemas/Economy",
"description": "Interest, discount, and shop slot helpers"
},
"probabilities": {
"type": "object",
"description": "Odds multipliers (Oops All 6s scales normal)",
"properties": {
"normal": {
"type": "number",
"description": "Base odds multiplier (1 by default)"
}
}
},
"pool_flags": {
"type": "object",
"description": "Run pool flags (e.g. Gros Michel extinct)",
"additionalProperties": {
"type": "boolean"
}
},
"skips": {
"type": "integer",
"description": "Number of blinds skipped this run"
},
"starting_deck_size": {
"type": "integer",
"description": "Starting deck size (Erosion baseline)"
},
"consumable_usage_total": {
"$ref": "#/components/schemas/ConsumableUsageTotal",
"description": "Tarot/Planet/Spectral use counts; omitted until first consumable is used"
},
"used_vouchers": {
"type": "object",
"description": "Vouchers used (name -> description)",
Expand Down Expand Up @@ -1047,9 +1087,129 @@
"type": "integer",
"description": "Current cost to reroll the shop"
},
"free_rerolls": {
"type": "integer",
"description": "Free shop rerolls remaining"
},
"chips": {
"type": "integer",
"description": "Current chips scored in this round"
},
"idol_card": {
"type": "object",
"description": "Current The Idol suit/rank target",
"properties": {
"suit": {
"$ref": "#/components/schemas/Suit"
},
"rank": {
"$ref": "#/components/schemas/Rank"
}
}
},
"mail_card": {
"type": "object",
"description": "Current Mail-In Rebate rank target",
"properties": {
"rank": {
"$ref": "#/components/schemas/Rank"
}
}
},
"ancient_card": {
"type": "object",
"description": "Current Ancient Joker suit target",
"properties": {
"suit": {
"$ref": "#/components/schemas/Suit"
}
}
},
"castle_card": {
"type": "object",
"description": "Current Castle suit target",
"properties": {
"suit": {
"$ref": "#/components/schemas/Suit"
}
}
}
}
},
"GameTag": {
"type": "object",
"description": "A held tag from skipping a blind",
"properties": {
"key": {
"type": "string",
"description": "Tag key (e.g. tag_coupon)"
},
"name": {
"type": "string",
"description": "Localized tag name"
},
"effect": {
"type": "string",
"description": "Localized tag effect description"
}
},
"required": [
"key",
"name",
"effect"
]
},
"Economy": {
"type": "object",
"description": "Interest, discount, and shop slot helpers",
"properties": {
"interest_cap": {
"type": "integer",
"description": "Money needed for max interest"
},
"interest_amount": {
"type": "integer",
"description": "Interest earned per $5 held"
},
"bankrupt_at": {
"type": "integer",
"description": "Minimum money before bankruptcy (Credit Card: -20)"
},
"discount_percent": {
"type": "integer",
"description": "Shop discount percent (Clearance Sale / Liquidation)"
},
"shop_slots": {
"type": "integer",
"description": "Number of main shop card slots (not the shop Area)"
}
},
"required": [
"interest_cap",
"interest_amount",
"bankrupt_at",
"discount_percent",
"shop_slots"
]
},
"ConsumableUsageTotal": {
"type": "object",
"description": "Consumable use counts for this run",
"properties": {
"tarot": {
"type": "integer"
},
"planet": {
"type": "integer"
},
"spectral": {
"type": "integer"
},
"tarot_planet": {
"type": "integer"
},
"all": {
"type": "integer"
}
}
},
Expand Down
Loading
Loading