feat: auto track after promotion challenge success
fix: failed promotion challenge can also lead to tracked promotions 0.1.13main
parent
bd85393ce8
commit
b1fbb57d83
|
|
@ -1,3 +1,8 @@
|
||||||
|
0.1.13
|
||||||
|
|
||||||
|
feat: auto track after promotion challenge success
|
||||||
|
fix: failed promotion challenge can also lead to tracked promotions
|
||||||
|
|
||||||
0.1.12
|
0.1.12
|
||||||
|
|
||||||
feat: add rank tracker
|
feat: add rank tracker
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,92 @@ function genOpenWins(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Deno.test("RankTracker don't promo after failed challenge", async () => {
|
||||||
|
const tracker = new TestRankTracker(undefined);
|
||||||
|
assertEquals(tracker.testGet(), {
|
||||||
|
state: undefined,
|
||||||
|
deltaMap: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalState = await tracker.updateState([{
|
||||||
|
bankaraMatchChallenge: {
|
||||||
|
winCount: 0,
|
||||||
|
loseCount: 3,
|
||||||
|
maxWinCount: 3,
|
||||||
|
maxLoseCount: 3,
|
||||||
|
state: "FAILED",
|
||||||
|
isPromo: true,
|
||||||
|
isUdemaeUp: false,
|
||||||
|
udemaeAfter: "B+",
|
||||||
|
earnedUdemaePoint: null,
|
||||||
|
},
|
||||||
|
historyDetails: {
|
||||||
|
nodes: [{
|
||||||
|
id: genId(1),
|
||||||
|
udemae: "B+",
|
||||||
|
judgement: "LOSE",
|
||||||
|
bankaraMatch: {
|
||||||
|
earnedUdemaePoint: null,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
id: genId(0),
|
||||||
|
udemae: "B+",
|
||||||
|
judgement: "LOSE",
|
||||||
|
bankaraMatch: {
|
||||||
|
earnedUdemaePoint: null,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
|
||||||
|
assertEquals(finalState, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("RankTracker autotrack after promotion", async () => {
|
||||||
|
const tracker = new TestRankTracker(undefined);
|
||||||
|
assertEquals(tracker.testGet(), {
|
||||||
|
state: undefined,
|
||||||
|
deltaMap: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalState = await tracker.updateState([{
|
||||||
|
bankaraMatchChallenge: {
|
||||||
|
winCount: 3,
|
||||||
|
loseCount: 0,
|
||||||
|
maxWinCount: 3,
|
||||||
|
maxLoseCount: 3,
|
||||||
|
state: "SUCCEEDED",
|
||||||
|
isPromo: true,
|
||||||
|
isUdemaeUp: true,
|
||||||
|
udemaeAfter: "A-",
|
||||||
|
earnedUdemaePoint: null,
|
||||||
|
},
|
||||||
|
historyDetails: {
|
||||||
|
nodes: [{
|
||||||
|
id: genId(1),
|
||||||
|
udemae: "B+",
|
||||||
|
judgement: "WIN",
|
||||||
|
bankaraMatch: {
|
||||||
|
earnedUdemaePoint: null,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
id: genId(0),
|
||||||
|
udemae: "B+",
|
||||||
|
judgement: "WIN",
|
||||||
|
bankaraMatch: {
|
||||||
|
earnedUdemaePoint: null,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
|
||||||
|
assertEquals(finalState, {
|
||||||
|
gameId: await gameId(genId(1)),
|
||||||
|
rank: "A-",
|
||||||
|
rankPoint: 200,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("RankTracker tracks promotion, ignoring INPROGRESS", async () => {
|
Deno.test("RankTracker tracks promotion, ignoring INPROGRESS", async () => {
|
||||||
const INIT_STATE = {
|
const INIT_STATE = {
|
||||||
gameId: await gameId(genId(0)),
|
gameId: await gameId(genId(0)),
|
||||||
|
|
@ -80,14 +166,14 @@ Deno.test("RankTracker tracks promotion, ignoring INPROGRESS", async () => {
|
||||||
},
|
},
|
||||||
historyDetails: {
|
historyDetails: {
|
||||||
nodes: [{
|
nodes: [{
|
||||||
id: await genId(1),
|
id: genId(1),
|
||||||
udemae: "B+",
|
udemae: "B+",
|
||||||
judgement: "WIN",
|
judgement: "WIN",
|
||||||
bankaraMatch: {
|
bankaraMatch: {
|
||||||
earnedUdemaePoint: null,
|
earnedUdemaePoint: null,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
id: await genId(0),
|
id: genId(0),
|
||||||
udemae: "B+",
|
udemae: "B+",
|
||||||
judgement: "WIN",
|
judgement: "WIN",
|
||||||
bankaraMatch: {
|
bankaraMatch: {
|
||||||
|
|
@ -131,21 +217,21 @@ Deno.test("RankTracker tracks promotion", async () => {
|
||||||
},
|
},
|
||||||
historyDetails: {
|
historyDetails: {
|
||||||
nodes: [{
|
nodes: [{
|
||||||
id: await genId(2),
|
id: genId(2),
|
||||||
udemae: "B+",
|
udemae: "B+",
|
||||||
judgement: "WIN",
|
judgement: "WIN",
|
||||||
bankaraMatch: {
|
bankaraMatch: {
|
||||||
earnedUdemaePoint: null,
|
earnedUdemaePoint: null,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
id: await genId(1),
|
id: genId(1),
|
||||||
udemae: "B+",
|
udemae: "B+",
|
||||||
judgement: "WIN",
|
judgement: "WIN",
|
||||||
bankaraMatch: {
|
bankaraMatch: {
|
||||||
earnedUdemaePoint: null,
|
earnedUdemaePoint: null,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
id: await genId(0),
|
id: genId(0),
|
||||||
udemae: "B+",
|
udemae: "B+",
|
||||||
judgement: "WIN",
|
judgement: "WIN",
|
||||||
bankaraMatch: {
|
bankaraMatch: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { RankState } from "./state.ts";
|
import { RankState } from "./state.ts";
|
||||||
import { BattleListNode, HistoryGroups, RankParam } from "./types.ts";
|
import {
|
||||||
|
BankaraMatchChallenge,
|
||||||
|
BattleListNode,
|
||||||
|
HistoryGroups,
|
||||||
|
RankParam,
|
||||||
|
} from "./types.ts";
|
||||||
import { gameId, parseHistoryDetailId } from "./utils.ts";
|
import { gameId, parseHistoryDetailId } from "./utils.ts";
|
||||||
|
|
||||||
const splusParams = () => {
|
const splusParams = () => {
|
||||||
|
|
@ -79,13 +84,14 @@ type Delta = {
|
||||||
rankAfter?: string;
|
rankAfter?: string;
|
||||||
rankPoint: number;
|
rankPoint: number;
|
||||||
isPromotion: boolean;
|
isPromotion: boolean;
|
||||||
|
isRankUp: boolean;
|
||||||
isChallengeFirst: boolean;
|
isChallengeFirst: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: auto rank up using rank params and delta.
|
// TODO: auto rank up using rank params and delta.
|
||||||
function addRank(state: RankState, delta: Delta): RankState {
|
function addRank(state: RankState, delta: Delta): RankState {
|
||||||
const { rank, rankPoint } = state;
|
const { rank, rankPoint } = state;
|
||||||
const { gameId, rankAfter, isPromotion, isChallengeFirst } = delta;
|
const { gameId, rankAfter, isPromotion, isRankUp, isChallengeFirst } = delta;
|
||||||
|
|
||||||
const rankIndex = RANK_PARAMS.findIndex((r) => r.rank === rank);
|
const rankIndex = RANK_PARAMS.findIndex((r) => r.rank === rank);
|
||||||
|
|
||||||
|
|
@ -112,7 +118,7 @@ function addRank(state: RankState, delta: Delta): RankState {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPromotion) {
|
if (isPromotion && isRankUp) {
|
||||||
const nextRankParam = RANK_PARAMS[rankIndex + 1];
|
const nextRankParam = RANK_PARAMS[rankIndex + 1];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -140,6 +146,92 @@ const battleTime = (id: string) => {
|
||||||
return new Date(dateStr);
|
return new Date(dateStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FlattenItem = {
|
||||||
|
gameId: string;
|
||||||
|
time: Date;
|
||||||
|
bankaraMatchChallenge: BankaraMatchChallenge | null;
|
||||||
|
index: number;
|
||||||
|
groupLength: number;
|
||||||
|
detail: BattleListNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateDeltaList(
|
||||||
|
state: RankState,
|
||||||
|
flatten: FlattenItem[],
|
||||||
|
) {
|
||||||
|
const index = flatten.findIndex((i) => i.gameId === state.gameId);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unProcessed = flatten.slice(index);
|
||||||
|
const deltaList: Delta[] = [];
|
||||||
|
let beforeGameId = state.gameId;
|
||||||
|
|
||||||
|
for (const i of unProcessed.slice(1)) {
|
||||||
|
if (!i.detail.bankaraMatch) {
|
||||||
|
throw new TypeError("bankaraMatch must be defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta: Delta = {
|
||||||
|
beforeGameId,
|
||||||
|
gameId: i.gameId,
|
||||||
|
rankPoint: 0,
|
||||||
|
isPromotion: false,
|
||||||
|
isRankUp: false,
|
||||||
|
isChallengeFirst: false,
|
||||||
|
};
|
||||||
|
beforeGameId = i.gameId;
|
||||||
|
if (i.bankaraMatchChallenge) {
|
||||||
|
// challenge
|
||||||
|
if (i.index === 0 && i.bankaraMatchChallenge.state !== "INPROGRESS") {
|
||||||
|
// last battle in challenge
|
||||||
|
delta = {
|
||||||
|
...delta,
|
||||||
|
rankAfter: i.bankaraMatchChallenge.udemaeAfter ?? undefined,
|
||||||
|
rankPoint: i.bankaraMatchChallenge.earnedUdemaePoint ?? 0,
|
||||||
|
isPromotion: i.bankaraMatchChallenge.isPromo ?? false,
|
||||||
|
isRankUp: i.bankaraMatchChallenge.isUdemaeUp ?? false,
|
||||||
|
isChallengeFirst: false,
|
||||||
|
};
|
||||||
|
} else if (i.index === i.groupLength - 1) {
|
||||||
|
// first battle in challenge
|
||||||
|
delta = {
|
||||||
|
...delta,
|
||||||
|
isChallengeFirst: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// open
|
||||||
|
delta = {
|
||||||
|
...delta,
|
||||||
|
// TODO: rankAfter should be undefined in open battle
|
||||||
|
rankAfter: i.detail.udemae,
|
||||||
|
rankPoint: i.detail.bankaraMatch?.earnedUdemaePoint ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
deltaList.push(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deltaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRankState(i: FlattenItem): RankState {
|
||||||
|
const rank = i.detail.udemae;
|
||||||
|
const param = RANK_PARAMS.find((i) => i.rank === rank);
|
||||||
|
|
||||||
|
if (!param) {
|
||||||
|
throw new Error(`Rank not found: ${rank}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
gameId: i.gameId,
|
||||||
|
rank,
|
||||||
|
rankPoint: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if state is empty, it will not track rank.
|
* if state is empty, it will not track rank.
|
||||||
*/
|
*/
|
||||||
|
|
@ -179,14 +271,11 @@ export class RankTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateState(
|
async updateState(
|
||||||
hisotry: HistoryGroups<BattleListNode>["nodes"],
|
history: HistoryGroups<BattleListNode>["nodes"],
|
||||||
) {
|
) {
|
||||||
if (!this.state) {
|
// history order by time. 0 is the oldest.
|
||||||
return;
|
const flatten: FlattenItem[] = await Promise.all(
|
||||||
}
|
history
|
||||||
|
|
||||||
const flatten = await Promise.all(
|
|
||||||
hisotry
|
|
||||||
.flatMap(
|
.flatMap(
|
||||||
({ historyDetails, bankaraMatchChallenge }) => {
|
({ historyDetails, bankaraMatchChallenge }) => {
|
||||||
return historyDetails.nodes.map((j, index) => ({
|
return historyDetails.nodes.map((j, index) => ({
|
||||||
|
|
@ -203,62 +292,51 @@ export class RankTracker {
|
||||||
.map((i) => i.gameId.then((gameId) => ({ ...i, gameId }))),
|
.map((i) => i.gameId.then((gameId) => ({ ...i, gameId }))),
|
||||||
);
|
);
|
||||||
|
|
||||||
const index = flatten.findIndex((i) => i.gameId === this.state!.gameId);
|
const gameIdTime = new Map<string | undefined, Date>(
|
||||||
|
flatten.map((i) => [i.gameId, i.time]),
|
||||||
|
);
|
||||||
|
|
||||||
if (index === -1) {
|
let curState: RankState | undefined;
|
||||||
|
const oldestPromotion = flatten.find((i) =>
|
||||||
|
i.bankaraMatchChallenge?.isPromo && i.bankaraMatchChallenge.isUdemaeUp
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* There are 4 cases:
|
||||||
|
* 1. state === undefined, oldestPromotion === undefined
|
||||||
|
* 2. state === undefined, oldestPromotion !== undefined
|
||||||
|
* 3. state !== undefined, oldestPromotion === undefined
|
||||||
|
* 4. state !== undefined, oldestPromotion !== undefined
|
||||||
|
*
|
||||||
|
* In case 1, we can't track rank. So we do nothing.
|
||||||
|
* In case 2, 3, we track rank by the non-undefined state.
|
||||||
|
* In case 4, we can track by the elder game.
|
||||||
|
*/
|
||||||
|
const thisStateTime = gameIdTime.get(this.state?.gameId);
|
||||||
|
if (!thisStateTime && !oldestPromotion) {
|
||||||
|
return;
|
||||||
|
} else if (thisStateTime && !oldestPromotion) {
|
||||||
|
curState = this.state;
|
||||||
|
} else if (!thisStateTime && oldestPromotion) {
|
||||||
|
curState = getRankState(oldestPromotion);
|
||||||
|
} else if (thisStateTime && oldestPromotion) {
|
||||||
|
if (thisStateTime <= oldestPromotion.time) {
|
||||||
|
curState = this.state;
|
||||||
|
} else {
|
||||||
|
curState = getRankState(oldestPromotion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!curState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unProcessed = flatten.slice(index);
|
const deltaList = generateDeltaList(curState, flatten);
|
||||||
const deltaList: Delta[] = [];
|
|
||||||
let beforeGameId = this.state.gameId;
|
|
||||||
|
|
||||||
for (const i of unProcessed.slice(1)) {
|
if (!deltaList) {
|
||||||
if (!i.detail.bankaraMatch) {
|
return;
|
||||||
throw new TypeError("bankaraMatch must be defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
let delta: Delta = {
|
|
||||||
beforeGameId,
|
|
||||||
gameId: i.gameId,
|
|
||||||
rankPoint: 0,
|
|
||||||
isPromotion: false,
|
|
||||||
isChallengeFirst: false,
|
|
||||||
};
|
|
||||||
beforeGameId = i.gameId;
|
|
||||||
if (i.bankaraMatchChallenge) {
|
|
||||||
// challenge
|
|
||||||
if (i.index === 0 && i.bankaraMatchChallenge.state !== "INPROGRESS") {
|
|
||||||
// last battle in challenge
|
|
||||||
delta = {
|
|
||||||
...delta,
|
|
||||||
rankAfter: i.bankaraMatchChallenge.udemaeAfter ?? undefined,
|
|
||||||
rankPoint: i.bankaraMatchChallenge.earnedUdemaePoint ?? 0,
|
|
||||||
isPromotion: i.bankaraMatchChallenge.isPromo ?? false,
|
|
||||||
isChallengeFirst: false,
|
|
||||||
};
|
|
||||||
} else if (i.index === i.groupLength - 1) {
|
|
||||||
// first battle in challenge
|
|
||||||
delta = {
|
|
||||||
...delta,
|
|
||||||
isChallengeFirst: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// open
|
|
||||||
delta = {
|
|
||||||
...delta,
|
|
||||||
// TODO: rankAfter should be undefined in open battle
|
|
||||||
rankAfter: i.detail.udemae,
|
|
||||||
rankPoint: i.detail.bankaraMatch?.earnedUdemaePoint ?? 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
deltaList.push(delta);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let curState = this.state;
|
|
||||||
|
|
||||||
for (const delta of deltaList) {
|
for (const delta of deltaList) {
|
||||||
this.deltaMap.set(delta.beforeGameId, delta);
|
this.deltaMap.set(delta.beforeGameId, delta);
|
||||||
curState = addRank(curState, delta);
|
curState = addRank(curState, delta);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
||||||
|
|
||||||
export const AGENT_NAME = "s3si.ts";
|
export const AGENT_NAME = "s3si.ts";
|
||||||
export const S3SI_VERSION = "0.1.12";
|
export const S3SI_VERSION = "0.1.13";
|
||||||
export const NSOAPP_VERSION = "2.3.1";
|
export const NSOAPP_VERSION = "2.3.1";
|
||||||
export const WEB_VIEW_VERSION = "1.0.0-5644e7a2";
|
export const WEB_VIEW_VERSION = "1.0.0-5644e7a2";
|
||||||
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";
|
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue