Files
CONSOLGAMES/tests/sim/loyalty.test.ts
T

147 lines
6.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
addStamp, createLoyaltyState, currentProgram, DEFAULT_TRACKS, generosity, issueCard,
joinProbability, returnVisitMultiplier, setProgram, StampTrack, validateTracks,
} from '../../src/sim/loyalty';
describe('loyalty', () => {
it('starts with 10 purchase stamps for a used game up to 10 €', () => {
const l = createLoyaltyState();
expect(currentProgram(l)).toEqual({
version: 1,
tracks: [{ stampType: 'buy_new', slots: 10, reward: { kind: 'free_used_game', maxValueCents: 1000 } }],
});
expect(generosity(currentProgram(l))).toBe(100);
expect(returnVisitMultiplier(currentProgram(l))).toBe(2);
expect(joinProbability(currentProgram(l))).toBeCloseTo(0.25);
});
it('issues numbered cards on the current program', () => {
const l = createLoyaltyState();
const card = issueCard(l);
expect(card).toEqual({ id: 'C0001', programVersion: 1, stamps: [0], pendingRewards: [], voucherCents: 0 });
expect(issueCard(l).id).toBe('C0002');
});
it('earns the reward on the last stamp and resets the track', () => {
const l = createLoyaltyState();
const card = issueCard(l);
for (let i = 0; i < 9; i++) expect(addStamp(l, card.id, 'buy_new')).toEqual([]);
expect(addStamp(l, card.id, 'buy_new')).toEqual([{ kind: 'free_used_game', maxValueCents: 1000 }]);
expect(card.stamps).toEqual([0]);
expect(card.pendingRewards).toHaveLength(1);
});
it('ignores stamp types the card does not track', () => {
const l = createLoyaltyState();
const card = issueCard(l);
expect(addStamp(l, card.id, 'trade')).toEqual([]);
expect(card.stamps).toEqual([0]);
});
it('supports two independent stamp tracks', () => {
const l = createLoyaltyState();
setProgram(l, [
{ stampType: 'buy_new', slots: 3, reward: { kind: 'discount_percent', percent: 20 } },
{ stampType: 'trade', slots: 3, reward: { kind: 'free_trade' } },
]);
const card = issueCard(l);
addStamp(l, card.id, 'buy_new');
addStamp(l, card.id, 'trade');
addStamp(l, card.id, 'trade');
expect(card.stamps).toEqual([1, 2]);
expect(addStamp(l, card.id, 'trade')).toEqual([{ kind: 'free_trade' }]);
});
it('turns voucher rewards into card credit', () => {
const l = createLoyaltyState();
setProgram(l, [{ stampType: 'buy_used', slots: 3, reward: { kind: 'voucher', valueCents: 500 } }]);
const card = issueCard(l);
for (let i = 0; i < 3; i++) addStamp(l, card.id, 'buy_used');
expect(card.voucherCents).toBe(500);
expect(card.pendingRewards).toEqual([]);
});
it('validates tracks', () => {
const reward = { kind: 'free_trade' } as const;
const t = (stampType: StampTrack['stampType'], slots: number): StampTrack => ({ stampType, slots, reward });
expect(validateTracks([])).toBe('1 ou 2 types de tampons');
expect(validateTracks([t('buy_new', 5), t('trade', 5), t('buy_back', 5)])).toBe('1 ou 2 types de tampons');
expect(validateTracks([t('buy_new', 5), t('buy_new', 5)])).toBe('Types de tampons en double');
expect(validateTracks([t('buy_new', 2)])).toBe('Entre 3 et 20 cases par type');
expect(validateTracks([t('buy_new', 21)])).toBe('Entre 3 et 20 cases par type');
expect(validateTracks([t('buy_new', 3)])).toBeNull();
expect(() => setProgram(createLoyaltyState(), [])).toThrow('1 ou 2 types de tampons');
});
it('keeps existing cards on the rules they were issued with', () => {
const l = createLoyaltyState();
const oldCard = issueCard(l);
setProgram(l, [{ stampType: 'buy_new', slots: 3, reward: { kind: 'voucher', valueCents: 500 } }]);
for (let i = 0; i < 3; i++) addStamp(l, oldCard.id, 'buy_new');
expect(oldCard.stamps).toEqual([3]);
expect(oldCard.voucherCents).toBe(0);
expect(issueCard(l).programVersion).toBe(2);
});
it('penalizes reputation when the program becomes less generous', () => {
const l = createLoyaltyState();
const lessGenerous = setProgram(l, [{ stampType: 'buy_new', slots: 20, reward: { kind: 'voucher', valueCents: 500 } }]);
expect(lessGenerous.reputationDelta).toBe(-2);
const moreGenerous = setProgram(l, [{ stampType: 'buy_new', slots: 5, reward: { kind: 'voucher', valueCents: 1000 } }]);
expect(moreGenerous.reputationDelta).toBe(0);
});
it('migrates a card to the current program once it completes a track', () => {
const l = createLoyaltyState();
const card = issueCard(l);
setProgram(l, [{ stampType: 'buy_new', slots: 3, reward: { kind: 'voucher', valueCents: 500 } }]);
for (let i = 0; i < 9; i++) expect(addStamp(l, card.id, 'buy_new')).toEqual([]);
expect(addStamp(l, card.id, 'buy_new')).toEqual([{ kind: 'free_used_game', maxValueCents: 1000 }]);
expect(card.programVersion).toBe(2);
expect(card.stamps).toEqual([0]);
expect(card.pendingRewards).toHaveLength(1);
addStamp(l, card.id, 'buy_new');
addStamp(l, card.id, 'buy_new');
expect(addStamp(l, card.id, 'buy_new')).toEqual([{ kind: 'voucher', valueCents: 500 }]);
expect(card.voucherCents).toBe(500);
});
it('stores deep copies of tracks', () => {
const l = createLoyaltyState();
const tracks: StampTrack[] = [{ stampType: 'buy_new', slots: 5, reward: { kind: 'free_trade' } }];
setProgram(l, tracks);
tracks[0].slots = 99;
expect(currentProgram(l).tracks[0].slots).toBe(5);
const first = l.programs[0].tracks[0];
expect(first).not.toBe(DEFAULT_TRACKS[0]);
});
it('rejects non-integer slots and non-positive rewards', () => {
const t = (slots: number, reward: StampTrack['reward']): StampTrack => ({ stampType: 'buy_new', slots, reward });
expect(validateTracks([t(3.5, { kind: 'free_trade' })])).toBe('Entre 3 et 20 cases par type');
expect(validateTracks([t(5, { kind: 'free_used_game', maxValueCents: 0 })])).toBe('Récompense invalide');
expect(validateTracks([t(5, { kind: 'voucher', valueCents: -1 })])).toBe('Récompense invalide');
expect(validateTracks([t(5, { kind: 'discount_percent', percent: 0 })])).toBe('Récompense invalide');
expect(validateTracks([t(5, { kind: 'discount_percent', percent: 101 })])).toBe('Récompense invalide');
expect(validateTracks([t(5, { kind: 'discount_percent', percent: 100 })])).toBeNull();
});
it('keeps partial progress on other tracks when migrating, capped to the new slots', () => {
const l = createLoyaltyState();
setProgram(l, [
{ stampType: 'buy_new', slots: 8, reward: { kind: 'free_trade' } },
{ stampType: 'trade', slots: 4, reward: { kind: 'free_trade' } },
]);
const card = issueCard(l);
card.stamps = [7, 3];
setProgram(l, [
{ stampType: 'buy_new', slots: 5, reward: { kind: 'free_trade' } },
{ stampType: 'trade', slots: 4, reward: { kind: 'free_trade' } },
]);
expect(addStamp(l, card.id, 'trade')).toEqual([{ kind: 'free_trade' }]);
expect(card.programVersion).toBe(3);
expect(card.stamps).toEqual([4, 0]);
});
});