diff --git a/packages/ludiek/src/plugins/currency/CurrencyPlugin.ts b/packages/ludiek/src/plugins/currency/CurrencyPlugin.ts index 86e2aef..81e46a5 100644 --- a/packages/ludiek/src/plugins/currency/CurrencyPlugin.ts +++ b/packages/ludiek/src/plugins/currency/CurrencyPlugin.ts @@ -99,14 +99,13 @@ export class CurrencyPlugin extends LudiekPlugin { /** * Try to spend the specified amount of currencies if you have it. * @return true if it was spent - * - * @todo(#61) Each currency is checked individually, meaning that if there are duplicate Ids it could lead to negative balance. */ public payCurrencies(currencies: Currency[]): boolean { - if (!this.hasCurrencies(currencies)) { + const simplified = this.simplifyCurrencyList(currencies); + if (!this.hasCurrencies(simplified)) { return false; } - this.loseCurrencies(currencies); + this.loseCurrencies(simplified); return true; } @@ -120,13 +119,9 @@ export class CurrencyPlugin extends LudiekPlugin { /** * Whether we have a list of currencies - * - * @todo(#61) Each currency is checked individually, meaning that if there are duplicate Ids the answer could be misleading. */ public hasCurrencies(currencies: Currency[]): boolean { - return currencies.every((c) => { - return this.hasCurrency(c); - }); + return this.simplifyCurrencyList(currencies).every((c) => this.hasCurrency(c)); } /** @@ -167,6 +162,14 @@ export class CurrencyPlugin extends LudiekPlugin { return id in this._state.balances; } + private simplifyCurrencyList(currencies: Currency[]): Currency[] { + const merged: Record = {}; + for (const c of currencies) { + merged[c.id] = (merged[c.id] ?? 0) + c.amount; + } + return Object.entries(merged).map(([id, amount]) => ({ id, amount })); + } + private validate(currency: Currency, action: string): void { if (!this.supportsCurrency(currency.id)) { throw new InvalidCurrencyError(`Cannot ${action} '${currency.amount}' of '${currency.id}'. Unknown currency`); diff --git a/packages/ludiek/tests/plugins/currency/currency-plugin-multiples.spec.ts b/packages/ludiek/tests/plugins/currency/currency-plugin-multiples.spec.ts index a12bb89..f11e100 100644 --- a/packages/ludiek/tests/plugins/currency/currency-plugin-multiples.spec.ts +++ b/packages/ludiek/tests/plugins/currency/currency-plugin-multiples.spec.ts @@ -92,4 +92,48 @@ describe('Multiple currencies', () => { // Assert expect(isPaid).toBe(false); }); + + it('does not pay duplicate currencies when combined amount exceeds balance', () => { + // Arrange + currency.gainCurrency({ id: '/currency/money', amount: 1 }); + + // Act — paying 1+1=2 money with only 1 in balance should fail + const isPaid = currency.payCurrencies([ + { id: '/currency/money', amount: 1 }, + { id: '/currency/money', amount: 1 }, + ]); + + // Assert + expect(isPaid).toBe(false); + expect(currency.getBalance('/currency/money')).toBe(1); + }); + + it('pays duplicate currencies when combined amount is within balance', () => { + // Arrange + currency.gainCurrency({ id: '/currency/money', amount: 3 }); + + // Act — paying 1+1=2 money with 3 in balance should succeed + const isPaid = currency.payCurrencies([ + { id: '/currency/money', amount: 1 }, + { id: '/currency/money', amount: 1 }, + ]); + + // Assert + expect(isPaid).toBe(true); + expect(currency.getBalance('/currency/money')).toBe(1); + }); + + it('hasCurrencies returns false for duplicate currencies exceeding balance', () => { + // Arrange + currency.gainCurrency({ id: '/currency/money', amount: 1 }); + + // Act + const has = currency.hasCurrencies([ + { id: '/currency/money', amount: 1 }, + { id: '/currency/money', amount: 1 }, + ]); + + // Assert + expect(has).toBe(false); + }); });