Splendor-Style Card Purchases with Gem Costs, Color Discounts and Concurrent Buys
Company: Brex
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Technical Screen
Build a small simulation of buying cards in a game modeled on the board game Splendor. Players hold gems in five colors: Blue (`B`), White (`W`), Green (`G`), Red (`R`) and Yellow (`Y`). Each card has a cost, given as a number of gems of each color, and a color of its own. The data model and the code structure are up to you.
Implement the three tasks below in order, then answer the concurrency follow-up. Each part builds on the previous one.
### Constraints and Clarifications
- Gem counts and card costs are non-negative integers. A color that a cost does not mention costs zero.
- A player has a gem inventory and a hand of purchased cards.
- The interview phrased the functions with `card` and `player_gems` arguments. Because a purchase also changes the player's hand, you may pass a player object that holds both the gems and the hand.
### Clarifying Questions
- Are cards drawn from a market shared by several players, so that a bought card disappears for everyone, or is card availability out of scope?
- Do spent gems go back to a shared bank, or do they simply leave the player's inventory?
- Should invalid input, such as a negative cost or an unknown color, raise an error or return `False`?
### Part 1 — Check whether a player can afford a card
Implement `can_purchase(card, player_gems)`, which returns `True` if the player's gems cover the card's cost and `False` otherwise.
```hint Compare color by color
Pick a representation for costs and inventories that turns the check into one pass over the colors, with no special case for colors a card does not use.
```
#### What This Part Should Cover
- A clear model for colors, gem counts and cards
- A per-color comparison that treats an unmentioned color as zero
- Tests for an exactly affordable card, a card short by one gem of one color, and a free card
### Part 2 — Buy a card
Implement `purchase(card, player_gems)`. It first checks whether the player can afford the card. If so, it adds the card to the player's hand, deducts the cost from the player's gems, and returns `True`; otherwise it returns `False`.
```hint All or nothing
A failed purchase must leave both the gems and the hand exactly as they were. Decide the order of the check and the updates so that this holds by construction.
```
#### What This Part Should Cover
- Reuse of the affordability check rather than a second copy of it
- No partial deduction when the purchase fails
- Tests of the gems and the hand after a successful purchase and after a failed one
### Part 3 — Discounts from owned cards
Add discounts. Each card already in the player's hand counts toward later purchases as one gem of that card's color: one card of a color pays for one gem of the same color. For example, a player whose hand holds 2 White cards and 1 Red card, and who wants a card costing 4 White and 1 Red, pays only 2 White gems. Update `can_purchase` and `purchase` accordingly.
```hint Derive or maintain
The discount is a function of the hand. Decide whether to recompute it at every purchase or keep it up to date as cards are bought, and what each choice costs.
```
#### Clarifying Questions for this Part
- When a color's discount exceeds the card's cost in that color, is the excess simply unused, or can it pay for other colors?
#### What This Part Should Cover
- The effective cost per color when the discount is smaller than, equal to, or larger than the cost
- Affordability and payment both based on the discounted cost
- The example from the prompt reproduced as a test
### Part 4 — Concurrent purchases by the same player
The system now runs in a multi-threaded or parallel environment, and the same player may send several purchase requests at the same moment. How would you change the design to keep it safe under concurrency and keep the player's data consistent?
```hint Find the race
Write down an interleaving of two purchases by one player that lets them spend the same gems twice.
```
```hint Where the state lives
The fix differs depending on whether the player's state lives in one process's memory or in a database shared by several servers. Consider both.
```
#### What This Part Should Cover
- The check-then-act race on the gems, and on the hand that drives the discount
- Making the check and every update of one purchase atomic per player
- Lock granularity, and the deadlock risk once a purchase touches more than one shared object
- Retries, and a test that exercises concurrent purchases
### What a Strong Answer Covers
- A data model that extends from Part 1 to Part 3 without a rewrite
- Small, readable functions with tests at every part, including the example from the prompt
- Correct edge cases: free cards, colors missing from a cost or an inventory, and discounts larger than costs
- A concurrency design that matches where the state lives, with its cost in contention and complexity
### Follow-up Questions
- If cards come from a market shared by several players, how do you stop two players from buying the same card?
- A client times out and resends a purchase request. How do you make sure the player is charged only once?
- How would you support one request that buys several cards, all or none?
Overview: Model a Splendor-style card game in code: check whether a player's five-color gem inventory can afford a card, make an all-or-nothing purchase, and apply discounts earned from cards already owned. A follow-up asks how to keep one player's simultaneous purchase requests safe and consistent.
Read the full Brex Software Engineer interview experience this question came from