coder / coder/balatrobot

fix!(lua.endpoints): buy/pack hang when Celestial pack contains Black Hole

Open
#199 0 comments 0 reactions 1 assignee Claimed by @S1M0N38 View on GitHub
completed-in-dev
Dominant language
Python
Stars
72
Forks
18
PR merge metrics
No merged PRs in 30d

Description

## Description

Buying or selecting from a Celestial booster pack causes the endpoint to hang indefinitely when the pack contains Black Hole (`c_black_hole`, a Spectral card) as its first card. The response is never sent and the request times out on the client side.

Both `buy` (opening the pack) and `pack` (selecting a card from an already-opened pack) are affected.

## Root Cause

Both `src/lua/endpoints/buy.lua` (~line 260) and `src/lua/endpoints/pack.lua` (~line 305) infer whether hand cards need to be dealt by checking the **first card's** `ability.set`:

```lua
local pack_key = G.pack_cards.cards[1].ability and G.pack_cards.cards[1].ability.set
local needs_hand = pack_key == "Tarot" or pack_key == "Spectral"
```

- **Black Hole** has `ability.set = "Spectral"` (it is a Spectral-type consumable)
- But Black Hole can appear in **Celestial packs** via the soul mechanism: when `create_card("Planet", ...)` is called for a Celestial pack, there is a ~0.3% chance per card (`pseudorandom > 0.997`) that it becomes Black Hole
- Celestial packs do NOT have `draw_hand = true` in SMODS's booster config (only Arcana and Spectral packs do)
- So when Black Hole is the first card, the code sets `needs_hand = true` and waits for `G.hand.cards` to be populated
- Hand cards are never dealt → the condition event spins forever → **endpoint hangs**

## Steps to Reproduce

Seed `S001250` naturally produces Black Hole in a Celestial pack via the soul mechanism after cycling 12 Celestial packs in the first shop.

1. Start a run: `start({"deck": "b_red", "stake": "stake_white", "seed": "S001250"})`
2. Select a blind: `select()`
3. Set chips and money: `set({"chips": 100000, "money": 999})`
4. Play a hand: `play({"cards": [0,1,2,3,4]})`
5. Cash out: `cash_out()`
6. Free a booster slot: `buy({"pack": 0})` → `pack({"skip": true})`
7. Cycle 12 Celestial packs: `add({"key": "p_celestial_normal_1"})` → `buy({"pack": 1})` → `pack({"skip": true})` × 12
8. Add the 13th Celestial pack: `add({"key": "p_celestial_normal_1"})`
9. Buy it: `buy({"pack": 1})`
10. **The request hangs indefinitely** — Black Hole (`set=Spectral`) is the first card

For the `pack` endpoint: load the attached fixture to reach `SMODS_BOOSTER_OPENED` state with Black Hole at index 0, then call `pack({"card": 0})` — this also hangs.

## Expected Behavior

The endpoints should return the gamestate response after the pack opens / selection completes, regardless of whether the first card is a Spectral type. Celestial packs never deal hand cards, so the endpoints should not wait for them.

## Actual Behavior

The request never returns. The log shows `Buying Booster 'Celestial Pack' for $4` but no corresponding OK or ERR response. The event loop spins on the `hand_ready` condition that never becomes true.

## Why This Bug Is Difficult to Reproduce

- Black Hole only appears in Celestial packs through the **soul mechanism**: a ~0.3% chance per card when creating Planet cards
- The bug only triggers when Black Hole is the **first** card in the pack, because the code only checks `G.pack_cards.cards[1]`
- Combined probability: roughly 0.05% per shop — automated bots hit it eventually, humans almost never
- Found via parallel search across 8 headless instances testing 2000 seeds

## Environment

- **OS:** macOS
- **Lovely version:** 0.8.0
- **SMODS version:** v1.0.0~BETA-1221a
- **BalatroBot commit:** 7a33403

## Files

- [53076.log](https://github.com/user-attachments/files/28904695/53076.log)
- [53076.req.jsonl.txt](https://github.com/user-attachments/files/28904694/53076.req.jsonl.txt)
- [53076.res.jsonl.txt](https://github.com/user-attachments/files/28904696/53076.res.jsonl.txt)
- [reproduce.py](https://github.com/user-attachments/files/28904697/reproduce.py)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.