feat: auto track after promotion challenge success

fix: failed promotion challenge can also lead to tracked promotions
0.1.13
main
spacemeowx2 2022-10-31 21:44:13 +08:00
parent bd85393ce8
commit b1fbb57d83
4 changed files with 234 additions and 65 deletions

View File

@ -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
feat: add rank tracker

View File

@ -53,6 +53,92 @@ function genOpenWins(
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 () => {
const INIT_STATE = {
gameId: await gameId(genId(0)),
@ -80,14 +166,14 @@ Deno.test("RankTracker tracks promotion, ignoring INPROGRESS", async () => {
},
historyDetails: {
nodes: [{
id: await genId(1),
id: genId(1),
udemae: "B+",
judgement: "WIN",
bankaraMatch: {
earnedUdemaePoint: null,
},
}, {
id: await genId(0),
id: genId(0),
udemae: "B+",
judgement: "WIN",
bankaraMatch: {
@ -131,21 +217,21 @@ Deno.test("RankTracker tracks promotion", async () => {
},
historyDetails: {
nodes: [{
id: await genId(2),
id: genId(2),
udemae: "B+",
judgement: "WIN",
bankaraMatch: {
earnedUdemaePoint: null,
},
}, {
id: await genId(1),
id: genId(1),
udemae: "B+",
judgement: "WIN",
bankaraMatch: {
earnedUdemaePoint: null,
},
}, {
id: await genId(0),
id: genId(0),
udemae: "B+",
judgement: "WIN",
bankaraMatch: {

View File

@ -1,5 +1,10 @@
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";
const splusParams = () => {
@ -79,13 +84,14 @@ type Delta = {
rankAfter?: string;
rankPoint: number;
isPromotion: boolean;
isRankUp: boolean;
isChallengeFirst: boolean;
};
// TODO: auto rank up using rank params and delta.
function addRank(state: RankState, delta: Delta): RankState {
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);
@ -112,7 +118,7 @@ function addRank(state: RankState, delta: Delta): RankState {
};
}
if (isPromotion) {
if (isPromotion && isRankUp) {
const nextRankParam = RANK_PARAMS[rankIndex + 1];
return {
@ -140,6 +146,92 @@ const battleTime = (id: string) => {
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.
*/
@ -179,14 +271,11 @@ export class RankTracker {
}
async updateState(
hisotry: HistoryGroups<BattleListNode>["nodes"],
history: HistoryGroups<BattleListNode>["nodes"],
) {
if (!this.state) {
return;
}
const flatten = await Promise.all(
hisotry
// history order by time. 0 is the oldest.
const flatten: FlattenItem[] = await Promise.all(
history
.flatMap(
({ historyDetails, bankaraMatchChallenge }) => {
return historyDetails.nodes.map((j, index) => ({
@ -203,62 +292,51 @@ export class RankTracker {
.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;
}
const unProcessed = flatten.slice(index);
const deltaList: Delta[] = [];
let beforeGameId = this.state.gameId;
const deltaList = generateDeltaList(curState, flatten);
for (const i of unProcessed.slice(1)) {
if (!i.detail.bankaraMatch) {
throw new TypeError("bankaraMatch must be defined");
if (!deltaList) {
return;
}
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) {
this.deltaMap.set(delta.beforeGameId, delta);
curState = addRank(curState, delta);

View File

@ -1,7 +1,7 @@
import type { StatInkPostBody, VsHistoryDetail } from "./types.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 WEB_VIEW_VERSION = "1.0.0-5644e7a2";
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";