mirror of
https://github.com/diangogav/EDOpro-server-ts
synced 2026-08-24 00:23:05 -04:00
feat(matchmaking): add edison to the ranked queue
Edison joins tcg and jtp as an accepted matchmaking format. Everything it needs already existed on its own — the rule set, the ban list, the bots — so this only wires it into the queue. The join string rides CTOS_JOIN_GAME's fixed utf16[20] pass field as "<token>,mm<5>#<7>", which caps a format's room token at 3 characters. "edison" (6) does not fit. Single duels reuse the existing "ed" alias; ranked human pairs play best-of-3, so "edm" is added to formatRuleMappings. It spreads the shared edisonRuleSet() payload, so the MATCH variant can never drift from "edison"/"ed" — one rule set, three tokens. Bot fallback picks from a new edison roster wired to the three Edison bots already shipped in the botlist. They are hidden there, kept out of the random pool, but stay reachable by explicit name — which is exactly how the roster requests them. Those rooms stay best-of-1: windbot cannot side deck. The existing it.each(MATCHMAKING_FORMATS) wire-budget guards pick edison up automatically and prove both tokens fit the pass field.
This commit is contained in:
parent
6e7baf2cc7
commit
baa735e950
8 changed files with 108 additions and 5 deletions
|
|
@ -87,6 +87,36 @@ describe("createMatchmakingRoom", () => {
|
|||
expect(room.hostInfo.rule).toBe(5);
|
||||
});
|
||||
|
||||
it('uses the "ed" token for edison single rooms (MR1 + edison banlist)', () => {
|
||||
const { room } = createMatchmakingRoom({
|
||||
format: "edison",
|
||||
rankedOverride: false,
|
||||
logger: makeLogger(),
|
||||
emitter: new EventEmitter(),
|
||||
});
|
||||
|
||||
expect(room.name.startsWith("ed,")).toBe(true);
|
||||
// Edison is Master Rule 1 over the open pool — the banlist defines the format.
|
||||
expect(room.hostInfo.rule).toBe(5);
|
||||
expect(room.hostInfo.duel_rule).toBe(1);
|
||||
expect(room.bestOf).toBe(1);
|
||||
});
|
||||
|
||||
it('uses the "edm" token for edison MATCH rooms (edison rules + best-of-3)', () => {
|
||||
const { room } = createMatchmakingRoom({
|
||||
format: "edison",
|
||||
rankedOverride: true,
|
||||
matchMode: true,
|
||||
logger: makeLogger(),
|
||||
emitter: new EventEmitter(),
|
||||
});
|
||||
|
||||
expect(room.bestOf).toBe(3);
|
||||
expect(room.isMatch).toBe(true);
|
||||
expect(room.name.startsWith("edm,")).toBe(true);
|
||||
expect(room.hostInfo.duel_rule).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps rooms best-of-1 when matchMode is omitted (bot fallback — windbot cannot side-deck)", () => {
|
||||
const { room } = createMatchmakingRoom({
|
||||
rankedOverride: false,
|
||||
|
|
|
|||
|
|
@ -22,21 +22,24 @@ import YGOProRoomList from "../../room/infrastructure/YGOProRoomList";
|
|||
* "tt" is the short alias of "toot" (RuleMappings): rule 5 + first TCG lflist —
|
||||
* TCG banlist over an OPEN pool. The old "to" (rule 1, scoped pool) bounced
|
||||
* cards without a TCG release (recent OCG printings, ot=0x9) at join.
|
||||
* "ed" is the short alias of "edison": rule 5 + edison lflist + Master Rule 1.
|
||||
*/
|
||||
export const FORMAT_ROOM_TOKEN: Record<MatchmakingFormat, string> = {
|
||||
tcg: "tt",
|
||||
jtp: "jtp",
|
||||
edison: "ed",
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-format MATCH (best-of-3) token, used for human pairs. Same banlist/rule
|
||||
* set as FORMAT_ROOM_TOKEN plus mode=MATCH + best_of=3 (RuleMappings: "tmr",
|
||||
* "jm"). "tmr" (3) sits at the 19-char wire ceiling like "jtp"; "jm" (2) has
|
||||
* one char of slack.
|
||||
* "jm", "edm"). "tmr" and "edm" (3) sit at the 19-char wire ceiling like "jtp";
|
||||
* "jm" (2) has one char of slack.
|
||||
*/
|
||||
export const FORMAT_ROOM_TOKEN_MATCH: Record<MatchmakingFormat, string> = {
|
||||
tcg: "tmr",
|
||||
jtp: "jm",
|
||||
edison: "edm",
|
||||
};
|
||||
|
||||
export interface CreateMatchmakingRoomInput {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ describe("MATCHMAKING_BOT_ROSTER", () => {
|
|||
expect(MATCHMAKING_BOT_ROSTER.jtp.length).toBe(2);
|
||||
});
|
||||
|
||||
it("edison roster has exactly 3 entries", () => {
|
||||
expect(MATCHMAKING_BOT_ROSTER.edison.length).toBe(3);
|
||||
});
|
||||
|
||||
it("all roster entries have non-empty name and deck strings", () => {
|
||||
for (const fmt of MATCHMAKING_FORMATS) {
|
||||
for (const pair of MATCHMAKING_BOT_ROSTER[fmt]) {
|
||||
|
|
@ -34,6 +38,17 @@ describe("MATCHMAKING_BOT_ROSTER", () => {
|
|||
expect(jtp[0]).toEqual({ name: "Joey", deck: "JTP" });
|
||||
expect(jtp[1]).toEqual({ name: "Yugi", deck: "Yugi" });
|
||||
});
|
||||
|
||||
it("edison roster pairs each archetype name with its Edison deck", () => {
|
||||
// Names and decks are verified against config/botlist.example.json, where
|
||||
// the Edison bots are `hidden` (excluded from the random pool) but still
|
||||
// reachable by explicit name — which is exactly how the roster requests them.
|
||||
expect(MATCHMAKING_BOT_ROSTER.edison).toEqual([
|
||||
{ name: "Blackwing", deck: "EdisonBlackwing" },
|
||||
{ name: "Lightsworn", deck: "EdisonLightsworn" },
|
||||
{ name: "Machina", deck: "EdisonMachina" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickBotFromRoster", () => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ export interface BotIdentity {
|
|||
* JTP roster: Joey plays JTP, Yugi plays Yugi. Both are in the server botlist
|
||||
* (config/botlist.example.json) so requestBot will find them by name.
|
||||
*
|
||||
* Edison roster: the three archetype bots shipped for the vs-AI screen —
|
||||
* "Blackwing" ↔ EdisonBlackwing.ydk, "Lightsworn" ↔ EdisonLightsworn.ydk,
|
||||
* "Machina" ↔ EdisonMachina.ydk. They are `hidden` in the botlist (kept out of
|
||||
* the random pool) but stay reachable by explicit name, which is how the roster
|
||||
* requests them.
|
||||
*
|
||||
* IMPORTANT: All deck strings must be legal for the active banlist of that format.
|
||||
* Adjust if a deck-check rejects any entry.
|
||||
*/
|
||||
|
|
@ -42,6 +48,11 @@ export const MATCHMAKING_BOT_ROSTER: Record<MatchmakingFormat, readonly BotIdent
|
|||
{ name: "Joey", deck: "JTP" },
|
||||
{ name: "Yugi", deck: "Yugi" },
|
||||
],
|
||||
edison: [
|
||||
{ name: "Blackwing", deck: "EdisonBlackwing" },
|
||||
{ name: "Lightsworn", deck: "EdisonLightsworn" },
|
||||
{ name: "Machina", deck: "EdisonMachina" },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { MATCHMAKING_FORMATS, MatchmakingFormat } from "./QueueEntry";
|
||||
|
||||
describe("MATCHMAKING_FORMATS", () => {
|
||||
it('is exactly ["tcg","jtp"] as a const array', () => {
|
||||
expect(MATCHMAKING_FORMATS).toEqual(["tcg", "jtp"]);
|
||||
it('is exactly ["tcg","jtp","edison"] as a const array', () => {
|
||||
expect(MATCHMAKING_FORMATS).toEqual(["tcg", "jtp", "edison"]);
|
||||
});
|
||||
|
||||
it("contains only non-empty, unique format strings", () => {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const MATCHED_GRACE_MS = 30_000;
|
|||
|
||||
/** All accepted matchmaking formats. Used as the single source of truth for the
|
||||
* Zod enum, type derivation, and per-format record maps. */
|
||||
export const MATCHMAKING_FORMATS = ["tcg", "jtp"] as const;
|
||||
export const MATCHMAKING_FORMATS = ["tcg", "jtp", "edison"] as const;
|
||||
export const SUPPORTED_QUEUE = "ranked" as const;
|
||||
|
||||
export type MatchmakingFormat = (typeof MATCHMAKING_FORMATS)[number];
|
||||
|
|
|
|||
|
|
@ -261,6 +261,36 @@ describe("ed mapping (edison short alias, used by AI duel join commands)", () =>
|
|||
});
|
||||
});
|
||||
|
||||
describe("edm mapping (edison best-of-3, used by matchmaking)", () => {
|
||||
it("maps the edison rule set to a best-of-3 match", () => {
|
||||
const findIndex = jest
|
||||
.spyOn(MercuryBanListMemoryRepository, "findIndexByAlias")
|
||||
.mockReturnValue(7);
|
||||
|
||||
const rule = formatRuleMappings.edm.get("edm");
|
||||
|
||||
// Same rule set as "edison"/"ed" — MR1 over the open pool with the edison
|
||||
// banlist and the 450s clock — plus MATCH + best-of-3 for ranked pairs.
|
||||
expect(rule).toEqual({
|
||||
rule: 5,
|
||||
lflist: 7,
|
||||
duel_rule: 1,
|
||||
time_limit: 450,
|
||||
mode: GameMode.MATCH,
|
||||
best_of: 3,
|
||||
});
|
||||
expect(findIndex).toHaveBeenCalledWith("edison");
|
||||
|
||||
findIndex.mockRestore();
|
||||
});
|
||||
|
||||
it("validates only the exact token", () => {
|
||||
expect(formatRuleMappings.edm.validate("edm")).toBe(true);
|
||||
expect(formatRuleMappings.edm.validate("ed")).toBe(false);
|
||||
expect(formatRuleMappings.edm.validate("edison")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("jtp-2007-03 mapping (JTP Advanced March 2007)", () => {
|
||||
it("maps the historical JTP variant to MR2 over the open JTP pool", () => {
|
||||
const findIndex = jest
|
||||
|
|
|
|||
|
|
@ -460,6 +460,20 @@ export const formatRuleMappings: RuleMappings = {
|
|||
return value === "ed";
|
||||
},
|
||||
},
|
||||
// "edm" is "edison" + best-of-3 match. Kept to 3 chars because it is the
|
||||
// matchmaking MATCH token for the edison queue, whose join string must fit
|
||||
// the utf16[20] wire field (token <= 3 chars — see MatchmakingRoomFactory).
|
||||
// Spreads the shared edison payload so the two can never drift apart.
|
||||
edm: {
|
||||
get: () => ({
|
||||
...edisonRuleSet(),
|
||||
mode: GameMode.MATCH,
|
||||
best_of: 3,
|
||||
}),
|
||||
validate: (value) => {
|
||||
return value === "edm";
|
||||
},
|
||||
},
|
||||
hat: {
|
||||
get: () => {
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue