feat: complete legacy-compatible auction system

This commit is contained in:
2026-07-25 10:43:49 +00:00
parent 26c3d5ce16
commit 93ae4df519
21 changed files with 1355 additions and 216 deletions
@@ -13,8 +13,7 @@ import {
createGameApiServer,
buildAuctionTimerKeys,
seedAuctionTimers,
buildTurnDaemonStreamKeys,
RedisTurnDaemonTransport,
DatabaseTurnDaemonTransport,
} from '@sammo-ts/game-api';
import { createTurnDaemonRuntime } from '@sammo-ts/game-engine';
import {
@@ -221,6 +220,7 @@ describe('auction integration flow', () => {
beforeAll(async () => {
await loadEnv();
process.env.SCENARIO = '908';
process.chdir(workspaceRoot);
await resetDatabase();
await resetRedis();
@@ -271,15 +271,15 @@ describe('auction integration flow', () => {
await gatewayClient.admin.profiles.upsert.mutate({
profile: 'che',
scenario: '2',
scenario: '908',
apiPort: Number(process.env.GAME_API_PORT ?? 14000),
status: 'RUNNING',
});
await gatewayClient.admin.profiles.installNow.mutate({
profileName: 'che:2',
profileName: 'che:908',
install: {
scenarioId: 2,
scenarioId: 908,
turnTermMinutes: 1,
sync: false,
fiction: 0,
@@ -300,7 +300,7 @@ describe('auction integration flow', () => {
});
const gatewayToken = await gatewayClient.auth.issueGameSession.mutate({
sessionToken: login.sessionToken,
profile: 'che:2',
profile: 'che:908',
});
const access = await gameClient.auth.exchangeGatewayToken.mutate({
gatewayToken: gatewayToken.gameToken,
@@ -326,7 +326,7 @@ describe('auction integration flow', () => {
const gatewayDatabaseUrl = resolvePostgresConfigFromEnv({ schema: 'public' }).url;
turnDaemon = await createTurnDaemonRuntime({
profile: 'che',
profileName: 'che:2',
profileName: 'che:908',
databaseUrl: gameDatabaseUrl,
gatewayDatabaseUrl,
});
@@ -405,6 +405,9 @@ describe('auction integration flow', () => {
where: { id: poorBidder.generalId },
data: { gold: 50, rice: 1000 },
});
await prisma.worldState.updateMany({
data: { currentMonth: 4 },
});
if (turnDaemon) {
await turnDaemon.lifecycle.stop('integration-test');
@@ -415,31 +418,24 @@ describe('auction integration flow', () => {
const gatewayDatabaseUrl = resolvePostgresConfigFromEnv({ schema: 'public' }).url;
turnDaemon = await createTurnDaemonRuntime({
profile: 'che',
profileName: 'che:2',
profileName: 'che:908',
databaseUrl: gameDatabaseUrl,
gatewayDatabaseUrl,
});
turnDaemonLoop = turnDaemon.lifecycle.start();
await sleep(500);
const now = new Date();
const initialCloseAt = new Date(now.getTime() + 10_000);
const auction = await prisma.auction.create({
data: {
type: 'BUY_RICE',
targetCode: null,
hostGeneralId: 0,
hostName: '시스템',
detail: {
amount: 500,
startBidAmount: 100,
isReverse: false,
availableLatestBidCloseDate: new Date(now.getTime() + 5 * 60_000).toISOString(),
},
status: 'OPEN',
closeAt: initialCloseAt,
},
const hostClient = createGameClient(gameUrl, gameServer.config.trpcPath, {
value: bidder3.accessToken,
});
const opened = await hostClient.auction.openBuyRice.mutate({
amount: 500,
closeTurnCnt: 1,
startBidAmount: 250,
finishBidAmount: 1000,
});
const auction = await prisma.auction.findUniqueOrThrow({ where: { id: opened.auctionId } });
const initialCloseAt = auction.closeAt;
const keys = buildAuctionTimerKeys(gameServer.config.profileName);
await seedAuctionTimers(prisma, redis, keys);
@@ -449,7 +445,7 @@ describe('auction integration flow', () => {
const bidder1Client = createGameClient(gameUrl, gameServer.config.trpcPath, {
value: bidder1.accessToken,
});
await bidder1Client.auction.bidBuyRice.mutate({ auctionId: auction.id, amount: 200 });
await bidder1Client.auction.bidBuyRice.mutate({ auctionId: auction.id, amount: 300 });
const bidRows = await prisma.auctionBid.findMany({ where: { auctionId: auction.id } });
expect(bidRows).toHaveLength(1);
@@ -478,10 +474,7 @@ describe('auction integration flow', () => {
});
await redis.zAdd(keys.timerKey, [{ score: finalizeAt.getTime(), value: String(auction.id) }]);
const transport = new RedisTurnDaemonTransport(redis, {
keys: buildTurnDaemonStreamKeys(gameServer.config.profileName),
requestTimeoutMs: 10_000,
});
const transport = new DatabaseTurnDaemonTransport(prisma, 30_000);
await prisma.$executeRaw(
GamePrisma.sql`
UPDATE auction
@@ -491,7 +484,7 @@ describe('auction integration flow', () => {
WHERE id = ${auction.id}
`
);
const result = await transport.requestCommand({ type: 'auctionFinalize', auctionId: auction.id }, 10_000);
const result = await transport.requestCommand({ type: 'auctionFinalize', auctionId: auction.id }, 30_000);
expect(result?.ok).toBe(true);
const finished = await prisma.auction.findUnique({
@@ -613,7 +606,7 @@ describe('auction integration flow', () => {
const gatewayDatabaseUrl = resolvePostgresConfigFromEnv({ schema: 'public' }).url;
turnDaemon = await createTurnDaemonRuntime({
profile: 'che',
profileName: 'che:2',
profileName: 'che:908',
databaseUrl: gameDatabaseUrl,
gatewayDatabaseUrl,
});
@@ -627,10 +620,7 @@ describe('auction integration flow', () => {
});
await redis.zAdd(keys.timerKey, [{ score: finalizeAt.getTime(), value: String(auction.id) }]);
const transport = new RedisTurnDaemonTransport(redis, {
keys: buildTurnDaemonStreamKeys(gameServer.config.profileName),
requestTimeoutMs: 10_000,
});
const transport = new DatabaseTurnDaemonTransport(prisma, 30_000);
await prisma.$executeRaw(
GamePrisma.sql`
UPDATE auction
@@ -640,7 +630,7 @@ describe('auction integration flow', () => {
WHERE id = ${auction.id}
`
);
const result = await transport.requestCommand({ type: 'auctionFinalize', auctionId: auction.id }, 10_000);
const result = await transport.requestCommand({ type: 'auctionFinalize', auctionId: auction.id }, 30_000);
expect(result?.ok).toBe(false);
const reopened = await prisma.auction.findUnique({
@@ -677,16 +667,20 @@ describe('auction integration flow', () => {
where: { id: bidderB.generalId },
data: { weaponCode: 'None', bookCode: 'None', horseCode: 'None', itemCode: 'None' },
});
await prisma.general.updateMany({
where: { [slotField]: uniquePair.keyA } as GamePrisma.GeneralWhereInput,
data: { [slotField]: 'None' } as GamePrisma.GeneralUpdateManyMutationInput,
});
await prisma.inheritancePoint.upsert({
where: { userId_key: { userId: bidderA.userId, key: 'previous' } },
update: { value: 5000 },
create: { userId: bidderA.userId, key: 'previous', value: 5000 },
update: { value: 100_000 },
create: { userId: bidderA.userId, key: 'previous', value: 100_000 },
});
await prisma.inheritancePoint.upsert({
where: { userId_key: { userId: bidderB.userId, key: 'previous' } },
update: { value: 5000 },
create: { userId: bidderB.userId, key: 'previous', value: 5000 },
update: { value: 100_000 },
create: { userId: bidderB.userId, key: 'previous', value: 100_000 },
});
if (turnDaemon) {
@@ -698,7 +692,7 @@ describe('auction integration flow', () => {
const gatewayDatabaseUrl = resolvePostgresConfigFromEnv({ schema: 'public' }).url;
turnDaemon = await createTurnDaemonRuntime({
profile: 'che',
profileName: 'che:2',
profileName: 'che:908',
databaseUrl: gameDatabaseUrl,
gatewayDatabaseUrl,
});
@@ -720,32 +714,39 @@ describe('auction integration flow', () => {
const keys = buildAuctionTimerKeys(gameServer.config.profileName);
await redis.del(keys.timerKey);
const limitCloseAt = new Date(now.getTime() + 60_000);
const auction = await prisma.auction.create({
data: {
type: 'UNIQUE_ITEM',
targetCode: uniquePair.keyA,
hostGeneralId: 0,
hostName: '시스템',
detail: {
startBidAmount: 200,
isReverse: false,
availableLatestBidCloseDate: limitCloseAt.toISOString(),
},
status: 'OPEN',
closeAt: new Date(now.getTime() + 2000),
},
const bidderAClient = createGameClient(gameUrl, gameServer.config.trpcPath, { value: bidderA.accessToken });
const bidderBClient = createGameClient(gameUrl, gameServer.config.trpcPath, { value: bidderB.accessToken });
const opened = await bidderAClient.auction.openUnique.mutate({
itemKey: uniquePair.keyA,
amount: 5000,
});
const limitCloseAt = new Date(now.getTime() + 60_000);
await prisma.$executeRaw(
GamePrisma.sql`
UPDATE auction
SET close_at = ${new Date(now.getTime() + 2000)},
detail = jsonb_set(
jsonb_set(
detail,
'{availableLatestBidCloseDate}',
to_jsonb(${limitCloseAt.toISOString()}::text)
),
'{remainCloseDateExtensionCnt}',
'0'::jsonb
),
updated_at = ${now}
WHERE id = ${opened.auctionId}
`
);
const auction = await prisma.auction.findUniqueOrThrow({ where: { id: opened.auctionId } });
await seedAuctionTimers(prisma, redis, keys);
const bidderAClient = createGameClient(gameUrl, gameServer.config.trpcPath, { value: bidderA.accessToken });
const bidderBClient = createGameClient(gameUrl, gameServer.config.trpcPath, { value: bidderB.accessToken });
await bidderAClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 300, tryExtendCloseDate: true });
await bidderBClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 350, tryExtendCloseDate: true });
await bidderAClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 420, tryExtendCloseDate: true });
await bidderBClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 500, tryExtendCloseDate: true });
await bidderBClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 5100, tryExtendCloseDate: true });
await bidderAClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 5200, tryExtendCloseDate: true });
await bidderBClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 5300, tryExtendCloseDate: true });
await bidderAClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 5400, tryExtendCloseDate: true });
await bidderBClient.auction.bidUnique.mutate({ auctionId: auction.id, amount: 5500, tryExtendCloseDate: true });
const afterBids = await prisma.auction.findUnique({
where: { id: auction.id },
@@ -761,10 +762,7 @@ describe('auction integration flow', () => {
});
await redis.zAdd(keys.timerKey, [{ score: finalizeAt.getTime(), value: String(auction.id) }]);
const transport = new RedisTurnDaemonTransport(redis, {
keys: buildTurnDaemonStreamKeys(gameServer.config.profileName),
requestTimeoutMs: 10_000,
});
const transport = new DatabaseTurnDaemonTransport(prisma, 30_000);
await prisma.$executeRaw(
GamePrisma.sql`
UPDATE auction
@@ -774,7 +772,7 @@ describe('auction integration flow', () => {
WHERE id = ${auction.id}
`
);
const result = await transport.requestCommand({ type: 'auctionFinalize', auctionId: auction.id }, 10_000);
const result = await transport.requestCommand({ type: 'auctionFinalize', auctionId: auction.id }, 30_000);
expect(result?.ok).toBe(true);
const winner = await prisma.general.findUnique({