From a5c68ad06bdb5edad9cd89ca2d7c0f819311b129 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Fri, 28 Oct 2022 15:32:35 +0800 Subject: [PATCH 01/12] refactor: move GameFetcher --- src/GameFetcher.ts | 211 +++++++++++++++++++++++++++++++++++++++++++ src/app.ts | 219 ++------------------------------------------- 2 files changed, 216 insertions(+), 214 deletions(-) create mode 100644 src/GameFetcher.ts diff --git a/src/GameFetcher.ts b/src/GameFetcher.ts new file mode 100644 index 0000000..00ee129 --- /dev/null +++ b/src/GameFetcher.ts @@ -0,0 +1,211 @@ +import { Mutex } from "../deps.ts"; +import { State } from "./state.ts"; +import { + getBankaraBattleHistories, + getBattleDetail, + getCoopDetail, + getCoopHistories, +} from "./splatnet3.ts"; +import { + BattleListNode, + ChallengeProgress, + CoopInfo, + CoopListNode, + Game, + HistoryGroups, + VsInfo, +} from "./types.ts"; +import { Cache, MemoryCache } from "./cache.ts"; +import { gameId } from "./utils.ts"; + +/** + * Fetch game and cache it. It also fetches bankara match challenge info. + */ +export class GameFetcher { + state: State; + cache: Cache; + lock: Record = {}; + bankaraLock = new Mutex(); + bankaraHistory?: HistoryGroups["nodes"]; + coopLock = new Mutex(); + coopHistory?: HistoryGroups["nodes"]; + + constructor( + { cache = new MemoryCache(), state }: { state: State; cache?: Cache }, + ) { + this.state = state; + this.cache = cache; + } + private getLock(id: string): Mutex { + let cur = this.lock[id]; + + if (!cur) { + cur = new Mutex(); + this.lock[id] = cur; + } + + return cur; + } + + getBankaraHistory() { + return this.bankaraLock.use(async () => { + if (this.bankaraHistory) { + return this.bankaraHistory; + } + + const { bankaraBattleHistories: { historyGroups } } = + await getBankaraBattleHistories( + this.state, + ); + + this.bankaraHistory = historyGroups.nodes; + + return this.bankaraHistory; + }); + } + getCoopHistory() { + return this.coopLock.use(async () => { + if (this.coopHistory) { + return this.coopHistory; + } + + const { coopResult: { historyGroups } } = await getCoopHistories( + this.state, + ); + + this.coopHistory = historyGroups.nodes; + + return this.coopHistory; + }); + } + async getCoopMetaById(id: string): Promise> { + const coopHistory = await this.getCoopHistory(); + const group = coopHistory.find((i) => + i.historyDetails.nodes.some((i) => i.id === id) + ); + + if (!group) { + return { + type: "CoopInfo", + listNode: null, + }; + } + + const listNode = group.historyDetails.nodes.find((i) => i.id === id) ?? + null; + + return { + type: "CoopInfo", + listNode, + }; + } + async getBattleMetaById(id: string): Promise> { + const gid = await gameId(id); + const bankaraHistory = await this.getBankaraHistory(); + const gameIdMap = new Map(); + + for (const i of bankaraHistory) { + for (const j of i.historyDetails.nodes) { + gameIdMap.set(j, await gameId(j.id)); + } + } + + const group = bankaraHistory.find((i) => + i.historyDetails.nodes.some((i) => gameIdMap.get(i) === gid) + ); + + if (!group) { + return { + type: "VsInfo", + challengeProgress: null, + bankaraMatchChallenge: null, + listNode: null, + }; + } + + const { bankaraMatchChallenge } = group; + const listNode = + group.historyDetails.nodes.find((i) => gameIdMap.get(i) === gid) ?? + null; + const index = group.historyDetails.nodes.indexOf(listNode!); + + let challengeProgress: null | ChallengeProgress = null; + if (bankaraMatchChallenge) { + const pastBattles = group.historyDetails.nodes.slice(0, index); + const { winCount, loseCount } = bankaraMatchChallenge; + challengeProgress = { + index, + winCount: winCount - + pastBattles.filter((i) => i.judgement == "WIN").length, + loseCount: loseCount - + pastBattles.filter((i) => + ["LOSE", "DEEMED_LOSE"].includes(i.judgement) + ).length, + }; + } + + return { + type: "VsInfo", + bankaraMatchChallenge, + listNode, + challengeProgress, + }; + } + cacheDetail( + id: string, + getter: () => Promise, + ): Promise { + const lock = this.getLock(id); + + return lock.use(async () => { + const cached = await this.cache.read(id); + if (cached) { + return cached; + } + + const detail = await getter(); + + await this.cache.write(id, detail); + + return detail; + }); + } + fetch(type: Game["type"], id: string): Promise { + switch (type) { + case "VsInfo": + return this.fetchBattle(id); + case "CoopInfo": + return this.fetchCoop(id); + default: + throw new Error(`Unknown game type: ${type}`); + } + } + async fetchBattle(id: string): Promise { + const detail = await this.cacheDetail( + id, + () => getBattleDetail(this.state, id).then((r) => r.vsHistoryDetail), + ); + const metadata = await this.getBattleMetaById(id); + + const game: VsInfo = { + ...metadata, + detail, + }; + + return game; + } + async fetchCoop(id: string): Promise { + const detail = await this.cacheDetail( + id, + () => getCoopDetail(this.state, id).then((r) => r.coopHistoryDetail), + ); + const metadata = await this.getCoopMetaById(id); + + const game: CoopInfo = { + ...metadata, + detail, + }; + + return game; + } +} diff --git a/src/app.ts b/src/app.ts index 5bf40c2..1d0d228 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,41 +1,24 @@ import { getBulletToken, getGToken, loginManually } from "./iksm.ts"; -import { MultiProgressBar, Mutex } from "../deps.ts"; +import { MultiProgressBar } from "../deps.ts"; import { DEFAULT_STATE, FileStateBackend, State, StateBackend, } from "./state.ts"; -import { - getBankaraBattleHistories, - getBattleDetail, - getBattleList, - getCoopDetail, - getCoopHistories, - isTokenExpired, -} from "./splatnet3.ts"; -import { - BattleListNode, - BattleListType, - ChallengeProgress, - CoopInfo, - CoopListNode, - Game, - GameExporter, - HistoryGroups, - VsInfo, -} from "./types.ts"; -import { Cache, FileCache, MemoryCache } from "./cache.ts"; +import { getBattleList, isTokenExpired } from "./splatnet3.ts"; +import { BattleListType, Game, GameExporter } from "./types.ts"; +import { Cache, FileCache } from "./cache.ts"; import { StatInkExporter } from "./exporters/stat.ink.ts"; import { FileExporter } from "./exporters/file.ts"; import { delay, - gameId, readline, RecoverableError, retryRecoverableError, showError, } from "./utils.ts"; +import { GameFetcher } from "./GameFetcher.ts"; export type Opts = { profilePath: string; @@ -54,198 +37,6 @@ export const DEFAULT_OPTS: Opts = { monitor: false, }; -/** - * Fetch game and cache it. - */ -class GameFetcher { - state: State; - cache: Cache; - lock: Record = {}; - bankaraLock = new Mutex(); - bankaraHistory?: HistoryGroups["nodes"]; - coopLock = new Mutex(); - coopHistory?: HistoryGroups["nodes"]; - - constructor( - { cache = new MemoryCache(), state }: { state: State; cache?: Cache }, - ) { - this.state = state; - this.cache = cache; - } - private getLock(id: string): Mutex { - let cur = this.lock[id]; - - if (!cur) { - cur = new Mutex(); - this.lock[id] = cur; - } - - return cur; - } - - getBankaraHistory() { - return this.bankaraLock.use(async () => { - if (this.bankaraHistory) { - return this.bankaraHistory; - } - - const { bankaraBattleHistories: { historyGroups } } = - await getBankaraBattleHistories( - this.state, - ); - - this.bankaraHistory = historyGroups.nodes; - - return this.bankaraHistory; - }); - } - getCoopHistory() { - return this.coopLock.use(async () => { - if (this.coopHistory) { - return this.coopHistory; - } - - const { coopResult: { historyGroups } } = await getCoopHistories( - this.state, - ); - - this.coopHistory = historyGroups.nodes; - - return this.coopHistory; - }); - } - async getCoopMetaById(id: string): Promise> { - const coopHistory = await this.getCoopHistory(); - const group = coopHistory.find((i) => - i.historyDetails.nodes.some((i) => i.id === id) - ); - - if (!group) { - return { - type: "CoopInfo", - listNode: null, - }; - } - - const listNode = group.historyDetails.nodes.find((i) => i.id === id) ?? - null; - - return { - type: "CoopInfo", - listNode, - }; - } - async getBattleMetaById(id: string): Promise> { - const gid = await gameId(id); - const bankaraHistory = await this.getBankaraHistory(); - const gameIdMap = new Map(); - - for (const i of bankaraHistory) { - for (const j of i.historyDetails.nodes) { - gameIdMap.set(j, await gameId(j.id)); - } - } - - const group = bankaraHistory.find((i) => - i.historyDetails.nodes.some((i) => gameIdMap.get(i) === gid) - ); - - if (!group) { - return { - type: "VsInfo", - challengeProgress: null, - bankaraMatchChallenge: null, - listNode: null, - }; - } - - const { bankaraMatchChallenge } = group; - const listNode = - group.historyDetails.nodes.find((i) => gameIdMap.get(i) === gid) ?? - null; - const index = group.historyDetails.nodes.indexOf(listNode!); - - let challengeProgress: null | ChallengeProgress = null; - if (bankaraMatchChallenge) { - const pastBattles = group.historyDetails.nodes.slice(0, index); - const { winCount, loseCount } = bankaraMatchChallenge; - challengeProgress = { - index, - winCount: winCount - - pastBattles.filter((i) => i.judgement == "WIN").length, - loseCount: loseCount - - pastBattles.filter((i) => - ["LOSE", "DEEMED_LOSE"].includes(i.judgement) - ).length, - }; - } - - return { - type: "VsInfo", - bankaraMatchChallenge, - listNode, - challengeProgress, - }; - } - cacheDetail( - id: string, - getter: () => Promise, - ): Promise { - const lock = this.getLock(id); - - return lock.use(async () => { - const cached = await this.cache.read(id); - if (cached) { - return cached; - } - - const detail = await getter(); - - await this.cache.write(id, detail); - - return detail; - }); - } - fetch(type: Game["type"], id: string): Promise { - switch (type) { - case "VsInfo": - return this.fetchBattle(id); - case "CoopInfo": - return this.fetchCoop(id); - default: - throw new Error(`Unknown game type: ${type}`); - } - } - async fetchBattle(id: string): Promise { - const detail = await this.cacheDetail( - id, - () => getBattleDetail(this.state, id).then((r) => r.vsHistoryDetail), - ); - const metadata = await this.getBattleMetaById(id); - - const game: VsInfo = { - ...metadata, - detail, - }; - - return game; - } - async fetchCoop(id: string): Promise { - const detail = await this.cacheDetail( - id, - () => getCoopDetail(this.state, id).then((r) => r.coopHistoryDetail), - ); - const metadata = await this.getCoopMetaById(id); - - const game: CoopInfo = { - ...metadata, - detail, - }; - - return game; - } -} - type Progress = { currentUrl?: string; current: number; From 297e73e19761d58993f1d96acddc08b48358aed4 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Fri, 28 Oct 2022 17:07:18 +0800 Subject: [PATCH 02/12] feat: add init rank --- dev_deps.ts | 1 + initRank.ts | 112 +++++++++++++++++++++++++++++++++++++++ src/RankTracker.test.ts | 7 +++ src/RankTracker.ts | 113 ++++++++++++++++++++++++++++++++++++++++ src/state.ts | 11 ++++ src/utils.ts | 6 ++- 6 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 dev_deps.ts create mode 100644 initRank.ts create mode 100644 src/RankTracker.test.ts create mode 100644 src/RankTracker.ts diff --git a/dev_deps.ts b/dev_deps.ts new file mode 100644 index 0000000..d68d3be --- /dev/null +++ b/dev_deps.ts @@ -0,0 +1 @@ +export { assertEquals } from "https://deno.land/std@0.160.0/testing/asserts.ts"; diff --git a/initRank.ts b/initRank.ts new file mode 100644 index 0000000..daf315a --- /dev/null +++ b/initRank.ts @@ -0,0 +1,112 @@ +/** + * If rankState in profile.json is not defined, it will be initialized. + */ +import { flags } from "./deps.ts"; +import { getBulletToken, getGToken } from "./src/iksm.ts"; +import { checkToken, getBattleDetail, getBattleList } from "./src/splatnet3.ts"; +import { gameId, readline } from "./src/utils.ts"; +import { FileStateBackend } from "./src/state.ts"; +import { BattleListType } from "./src/types.ts"; +import { RANK_PARAMS } from "./src/RankTracker.ts"; + +const parseArgs = (args: string[]) => { + const parsed = flags.parse(args, { + string: ["profilePath"], + alias: { + "help": "h", + "profilePath": ["p", "profile-path"], + }, + }); + return parsed; +}; + +const opts = parseArgs(Deno.args); +if (opts.help) { + console.log( + `Usage: deno run -A ${Deno.mainModule} [options] + + Options: + --profile-path , -p Path to config file (default: ./profile.json) + --help Show this help message and exit`, + ); + Deno.exit(0); +} + +const stateBackend = new FileStateBackend(opts.profilePath ?? "./profile.json"); +let state = await stateBackend.read(); + +if (state.rankState) { + console.log("rankState is already initialized."); + Deno.exit(0); +} + +if (!await checkToken(state)) { + const sessionToken = state.loginState?.sessionToken; + + if (!sessionToken) { + throw new Error("Session token is not set."); + } + + const { webServiceToken, userCountry, userLang } = await getGToken({ + fApi: state.fGen, + sessionToken, + }); + + const bulletToken = await getBulletToken({ + webServiceToken, + userLang, + userCountry, + appUserAgent: state.appUserAgent, + }); + + state = { + ...state, + loginState: { + ...state.loginState, + gToken: webServiceToken, + bulletToken, + }, + userLang: state.userLang ?? userLang, + userCountry: state.userCountry ?? userCountry, + }; + await stateBackend.write(state); +} + +const battleList = await getBattleList(state, BattleListType.Bankara); +if (battleList.length === 0) { + console.log("No anarchy battle found. Did you play anarchy?"); + Deno.exit(0); +} +const { vsHistoryDetail: detail } = await getBattleDetail(state, battleList[0]); + +console.log( + `Your latest battle is played at ${ + new Date(detail.playedTime).toLocaleString() + }. Please enter your rank after this battle(format: RANK,POINT. S+0,300):`, +); + +while (true) { + const userInput = await readline(); + const [rank, point] = userInput.split(","); + const pointNumber = parseInt(point); + + if (!RANK_PARAMS.find((i) => i.rank === rank)) { + console.log("Invalid rank. Please enter again:"); + } else if (isNaN(pointNumber)) { + console.log("Invalid point. Please enter again:"); + } else { + state = { + ...state, + rankState: { + gameId: await gameId(detail.id), + rank, + rankPoint: pointNumber, + }, + }; + + break; + } +} + +await stateBackend.write(state); +console.log("rankState is initialized."); diff --git a/src/RankTracker.test.ts b/src/RankTracker.test.ts new file mode 100644 index 0000000..4d99fa0 --- /dev/null +++ b/src/RankTracker.test.ts @@ -0,0 +1,7 @@ +import { RankTracker } from "./RankTracker.ts"; +import { assertEquals } from "../dev_deps.ts"; + +Deno.test("RankTracker", () => { + const tracker = new RankTracker(); + assertEquals(tracker, new RankTracker()); +}); diff --git a/src/RankTracker.ts b/src/RankTracker.ts new file mode 100644 index 0000000..e2b14c0 --- /dev/null +++ b/src/RankTracker.ts @@ -0,0 +1,113 @@ +import { RankState } from "./state.ts"; +import { GameFetcher } from "./GameFetcher.ts"; + +type RankParam = { + rank: string; + pointRange: [number, number]; + entrance: number; + openWin: number; + openLose: number; + rankUp?: boolean; +}; + +const splusParams = () => { + const out: RankParam[] = []; + + for (let i = 0; i < 50; i++) { + const level = i % 10; + const item: RankParam = { + rank: `S+${i}`, + pointRange: [300 + level * 350, 300 + (level + 1) * 350], + entrance: 160, + openWin: 8, + openLose: 5, + }; + if (level === 9) { + item.rankUp = true; + } + out.push(item); + } + + out.push({ + rank: "S+50", + pointRange: [0, 9999], + entrance: 160, + openWin: 8, + openLose: 5, + }); + + return out; +}; + +export const RANK_PARAMS: RankParam[] = [{ + rank: "C-", + pointRange: [0, 200], + entrance: 0, + openWin: 8, + openLose: 1, +}, { + rank: "C", + pointRange: [200, 400], + entrance: 20, + openWin: 8, + openLose: 1, +}, { + rank: "C+", + pointRange: [400, 600], + entrance: 40, + openWin: 8, + openLose: 1, + rankUp: true, +}, { + rank: "B-", + pointRange: [100, 350], + entrance: 55, + openWin: 8, + openLose: 2, +}, { + rank: "B", + pointRange: [350, 600], + entrance: 70, + openWin: 8, + openLose: 2, +}, { + rank: "B+", + pointRange: [600, 850], + entrance: 85, + openWin: 8, + openLose: 2, + rankUp: true, +}, { + rank: "A-", + pointRange: [200, 500], + entrance: 100, + openWin: 8, + openLose: 3, +}, { + rank: "A", + pointRange: [500, 800], + entrance: 110, + openWin: 8, + openLose: 3, +}, { + rank: "A+", + pointRange: [800, 1100], + entrance: 120, + openWin: 8, + openLose: 3, + rankUp: true, +}, { + rank: "S", + pointRange: [300, 1000], + entrance: 150, + openWin: 8, + openLose: 4, + rankUp: true, +}, ...splusParams()]; + +/** + * if state is empty, it will not track rank. + */ +export class RankTracker { + constructor(private state?: RankState) {} +} diff --git a/src/state.ts b/src/state.ts index fd611de..b03794a 100644 --- a/src/state.ts +++ b/src/state.ts @@ -3,6 +3,15 @@ export type LoginState = { gToken?: string; bulletToken?: string; }; +export type RankState = { + // generated by gameId(battle.id) + // If the gameId does not exist, the tracker will assume that the user has + // not played a bankara match. And it will start tracking from the first match + gameId?: string; + // C-, B, A+, S, S+0, S+12 + rank: string; + rankPoint: number; +}; export type State = { loginState?: LoginState; fGen: string; @@ -10,6 +19,8 @@ export type State = { userLang?: string; userCountry?: string; + rankState?: RankState; + cacheDir: string; // Exporter config diff --git a/src/utils.ts b/src/utils.ts index cb4bf72..de5619d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,9 +19,11 @@ export function urlBase64Decode(data: string) { ); } -export async function readline() { +export async function readline( + { skipEmpty = true }: { skipEmpty?: boolean } = {}, +) { for await (const line of stdinLines) { - if (line !== "") { + if (!skipEmpty || line !== "") { return line; } } From 9f383ae76c7f7b5a46022f6cbb7b3817be662033 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Fri, 28 Oct 2022 20:45:56 +0800 Subject: [PATCH 03/12] feat: add RankTracker --- initRank.ts | 2 +- src/GameFetcher.ts | 23 ++++- src/RankTracker.ts | 247 +++++++++++++++++++++++++++++++++++---------- src/app.ts | 9 ++ src/state.ts | 4 +- src/types.ts | 31 +++++- 6 files changed, 255 insertions(+), 61 deletions(-) diff --git a/initRank.ts b/initRank.ts index daf315a..57d13ff 100644 --- a/initRank.ts +++ b/initRank.ts @@ -80,7 +80,7 @@ if (battleList.length === 0) { const { vsHistoryDetail: detail } = await getBattleDetail(state, battleList[0]); console.log( - `Your latest battle is played at ${ + `Your latest anarchy battle is played at ${ new Date(detail.playedTime).toLocaleString() }. Please enter your rank after this battle(format: RANK,POINT. S+0,300):`, ); diff --git a/src/GameFetcher.ts b/src/GameFetcher.ts index 00ee129..693b80c 100644 --- a/src/GameFetcher.ts +++ b/src/GameFetcher.ts @@ -1,5 +1,5 @@ import { Mutex } from "../deps.ts"; -import { State } from "./state.ts"; +import { RankState, State } from "./state.ts"; import { getBankaraBattleHistories, getBattleDetail, @@ -17,6 +17,7 @@ import { } from "./types.ts"; import { Cache, MemoryCache } from "./cache.ts"; import { gameId } from "./utils.ts"; +import { RankTracker } from "./RankTracker.ts"; /** * Fetch game and cache it. It also fetches bankara match challenge info. @@ -24,6 +25,8 @@ import { gameId } from "./utils.ts"; export class GameFetcher { state: State; cache: Cache; + rankTracker: RankTracker; + lock: Record = {}; bankaraLock = new Mutex(); bankaraHistory?: HistoryGroups["nodes"]; @@ -35,6 +38,7 @@ export class GameFetcher { ) { this.state = state; this.cache = cache; + this.rankTracker = new RankTracker(state.rankState); } private getLock(id: string): Mutex { let cur = this.lock[id]; @@ -47,6 +51,21 @@ export class GameFetcher { return cur; } + setRankState(state: RankState | undefined) { + this.rankTracker.setState(state); + } + + async updateRank(): Promise { + const finalState = await this.rankTracker.updateState( + await this.getBankaraHistory(), + ); + return finalState; + } + + getRankStateById(id: string): Promise { + return this.rankTracker.getRankStateById(id); + } + getBankaraHistory() { return this.bankaraLock.use(async () => { if (this.bankaraHistory) { @@ -120,6 +139,7 @@ export class GameFetcher { challengeProgress: null, bankaraMatchChallenge: null, listNode: null, + rankState: null, }; } @@ -149,6 +169,7 @@ export class GameFetcher { bankaraMatchChallenge, listNode, challengeProgress, + rankState: await this.rankTracker.getRankStateById(id) ?? null, }; } cacheDetail( diff --git a/src/RankTracker.ts b/src/RankTracker.ts index e2b14c0..f537d8c 100644 --- a/src/RankTracker.ts +++ b/src/RankTracker.ts @@ -1,14 +1,6 @@ import { RankState } from "./state.ts"; -import { GameFetcher } from "./GameFetcher.ts"; - -type RankParam = { - rank: string; - pointRange: [number, number]; - entrance: number; - openWin: number; - openLose: number; - rankUp?: boolean; -}; +import { BattleListNode, HistoryGroups, RankParam } from "./types.ts"; +import { gameId, parseHistoryDetailId } from "./utils.ts"; const splusParams = () => { const out: RankParam[] = []; @@ -18,12 +10,10 @@ const splusParams = () => { const item: RankParam = { rank: `S+${i}`, pointRange: [300 + level * 350, 300 + (level + 1) * 350], - entrance: 160, - openWin: 8, - openLose: 5, + charge: 160, }; if (level === 9) { - item.rankUp = true; + item.promotion = true; } out.push(item); } @@ -31,9 +21,7 @@ const splusParams = () => { out.push({ rank: "S+50", pointRange: [0, 9999], - entrance: 160, - openWin: 8, - openLose: 5, + charge: 160, }); return out; @@ -42,72 +30,227 @@ const splusParams = () => { export const RANK_PARAMS: RankParam[] = [{ rank: "C-", pointRange: [0, 200], - entrance: 0, - openWin: 8, - openLose: 1, + charge: 0, }, { rank: "C", pointRange: [200, 400], - entrance: 20, - openWin: 8, - openLose: 1, + charge: 20, }, { rank: "C+", pointRange: [400, 600], - entrance: 40, - openWin: 8, - openLose: 1, - rankUp: true, + charge: 40, + promotion: true, }, { rank: "B-", pointRange: [100, 350], - entrance: 55, - openWin: 8, - openLose: 2, + charge: 55, }, { rank: "B", pointRange: [350, 600], - entrance: 70, - openWin: 8, - openLose: 2, + charge: 70, }, { rank: "B+", pointRange: [600, 850], - entrance: 85, - openWin: 8, - openLose: 2, - rankUp: true, + charge: 85, + promotion: true, }, { rank: "A-", pointRange: [200, 500], - entrance: 100, - openWin: 8, - openLose: 3, + charge: 100, }, { rank: "A", pointRange: [500, 800], - entrance: 110, - openWin: 8, - openLose: 3, + charge: 110, }, { rank: "A+", pointRange: [800, 1100], - entrance: 120, - openWin: 8, - openLose: 3, - rankUp: true, + charge: 120, + promotion: true, }, { rank: "S", pointRange: [300, 1000], - entrance: 150, - openWin: 8, - openLose: 4, - rankUp: true, + charge: 150, + promotion: true, }, ...splusParams()]; +type Delta = { + beforeGameId: string; + gameId: string; + rankPoint: number; + isRankUp: boolean; + isChallengeFirst: boolean; +}; + +function addRank(state: RankState, delta: Delta): RankState { + const { rank, rankPoint } = state; + const { gameId, isRankUp, isChallengeFirst } = delta; + + const rankIndex = RANK_PARAMS.findIndex((r) => r.rank === rank); + + if (rankIndex === -1) { + throw new Error(`Rank not found: ${rank}`); + } + + const rankParam = RANK_PARAMS[rankIndex]; + + if (isChallengeFirst) { + return { + gameId, + rank, + rankPoint: rankPoint - rankParam.charge, + }; + } + + // S+50 is the highest rank + if (rankIndex === RANK_PARAMS.length - 1) { + return { + gameId, + rank, + rankPoint: Math.min(rankPoint + delta.rankPoint, rankParam.pointRange[1]), + }; + } + + if (isRankUp) { + const nextRankParam = RANK_PARAMS[rankIndex + 1]; + + return { + gameId, + rank: nextRankParam.rank, + rankPoint: nextRankParam.pointRange[0], + }; + } + + return { + gameId, + rank, + rankPoint: rankPoint + delta.rankPoint, + }; +} + +const battleTime = (id: string) => { + const { timestamp } = parseHistoryDetailId(id); + + const dateStr = timestamp.replace( + /(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, + "$1-$2-$3T$4:$5:$6Z", + ); + + return new Date(dateStr); +}; + /** * if state is empty, it will not track rank. */ export class RankTracker { - constructor(private state?: RankState) {} + // key: privous game id + deltaMap: Map = new Map(); + + constructor(private state: RankState | undefined) {} + + async getRankStateById(id: string): Promise { + if (!this.state) { + return undefined; + } + const gid = await gameId(id); + + let cur = this.state; + while (cur.gameId !== gid) { + const delta = this.deltaMap.get(cur.gameId); + if (!delta) { + throw new Error("Delta not found"); + } + cur = addRank(cur, delta); + } + + return cur; + } + + setState(state: RankState | undefined) { + this.state = state; + } + + async updateState( + hisotry: HistoryGroups["nodes"], + ) { + if (!this.state) { + return; + } + + const flatten = await Promise.all( + hisotry + .flatMap( + ({ historyDetails, bankaraMatchChallenge }) => { + return historyDetails.nodes.map((j, index) => ({ + time: battleTime(j.id), + gameId: gameId(j.id), + bankaraMatchChallenge, + index, + groupLength: historyDetails.nodes.length, + detail: j, + })); + }, + ) + .sort((a, b) => a.time.getTime() - b.time.getTime()) + .map((i) => i.gameId.then((gameId) => ({ ...i, gameId }))), + ); + + const index = flatten.findIndex((i) => i.gameId === this.state!.gameId); + + if (index === -1) { + return; + } + + const unProcessed = flatten.slice(index); + const deltaList: Delta[] = []; + let beforeGameId = this.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, + isRankUp: false, + isChallengeFirst: false, + }; + beforeGameId = i.gameId; + // challenge + if (i.bankaraMatchChallenge) { + if (i.index === 0 && i.bankaraMatchChallenge.state !== "INPROGRESS") { + // last battle in challenge + delta = { + ...delta, + rankPoint: i.bankaraMatchChallenge.earnedUdemaePoint ?? 0, + isRankUp: i.bankaraMatchChallenge.isUdemaeUp ?? false, + isChallengeFirst: i.index === 0, + }; + } else if (i.index === i.groupLength - 1) { + // first battle in challenge + delta = { + ...delta, + isChallengeFirst: true, + }; + } + } else { + delta = { + ...delta, + rankPoint: i.detail.bankaraMatch?.earnedUdemaePoint, + }; + } + + deltaList.push(delta); + } + + let curState = this.state; + + for (const delta of deltaList) { + this.deltaMap.set(delta.beforeGameId, delta); + curState = addRank(curState, delta); + } + + return curState; + } } diff --git a/src/app.ts b/src/app.ts index 1d0d228..e14fdd1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -169,6 +169,8 @@ export class App { state: this.state, }); + const finalRankState = await fetcher.updateRank(); + await Promise.all( exporters.map((e) => showError( @@ -189,6 +191,13 @@ export class App { ), ); + // save rankState only if all exporters succeeded + fetcher.setRankState(finalRankState); + await this.writeState({ + ...this.state, + rankState: finalRankState, + }); + endBar(); } diff --git a/src/state.ts b/src/state.ts index b03794a..332f023 100644 --- a/src/state.ts +++ b/src/state.ts @@ -5,9 +5,9 @@ export type LoginState = { }; export type RankState = { // generated by gameId(battle.id) - // If the gameId does not exist, the tracker will assume that the user has + // TODO: If the gameId does not exist, the tracker will assume that the user has // not played a bankara match. And it will start tracking from the first match - gameId?: string; + gameId: string; // C-, B, A+, S, S+0, S+12 rank: string; rankPoint: number; diff --git a/src/types.ts b/src/types.ts index 3cfe87b..0702591 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { RankState } from "./state.ts"; + export enum Queries { HomeQuery = "dba47124d5ec3090c97ba17db5d2f4b3", LatestBattleHistoriesQuery = "7d8b560e31617e981cf7c8aa1ca13a00", @@ -43,6 +45,15 @@ export type BattleListNode = { id: string; udemae: string; judgement: "LOSE" | "WIN" | "DEEMED_LOSE" | "EXEMPTED_LOSE" | "DRAW"; + nextHistoryDetail: null | { + id: string; + }; + previousHistoryDetail: null | { + id: string; + }; + bankaraMatch: null | { + earnedUdemaePoint: number; + }; }; export type CoopListNode = { id: string; @@ -103,6 +114,7 @@ export type VsInfo = { listNode: null | BattleListNode; bankaraMatchChallenge: null | BankaraMatchChallenge; challengeProgress: null | ChallengeProgress; + rankState: null | RankState; detail: VsHistoryDetail; }; // Salmon run @@ -164,6 +176,12 @@ export type GameExporter< exportGame: (game: T) => Promise<{ url?: string }>; }; +export type BankaraBattleHistories = { + bankaraBattleHistories: { + historyGroups: HistoryGroups; + }; +}; + export type RespMap = { [Queries.HomeQuery]: { currentPlayer: { @@ -193,11 +211,7 @@ export type RespMap = { historyGroups: HistoryGroups; }; }; - [Queries.BankaraBattleHistoriesQuery]: { - bankaraBattleHistories: { - historyGroups: HistoryGroups; - }; - }; + [Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories; [Queries.PrivateBattleHistoriesQuery]: { privateBattleHistories: { historyGroups: HistoryGroups; @@ -330,3 +344,10 @@ export type StatInkPostResponse = { id: string; url: string; }; + +export type RankParam = { + rank: string; + pointRange: [number, number]; + charge: number; + promotion?: boolean; +}; From 1d0f095ad7b27487562ec949d93323fc1e462a05 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Fri, 28 Oct 2022 21:32:54 +0800 Subject: [PATCH 04/12] test: add RankTracker test --- src/RankTracker.test.ts | 128 +++++++++++++++++++++++++++++++++++++++- src/RankTracker.ts | 4 +- 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/RankTracker.test.ts b/src/RankTracker.test.ts index 4d99fa0..91c23c4 100644 --- a/src/RankTracker.test.ts +++ b/src/RankTracker.test.ts @@ -1,7 +1,129 @@ import { RankTracker } from "./RankTracker.ts"; import { assertEquals } from "../dev_deps.ts"; +import { BattleListNode } from "./types.ts"; +import { base64 } from "../deps.ts"; +import { gameId } from "./utils.ts"; -Deno.test("RankTracker", () => { - const tracker = new RankTracker(); - assertEquals(tracker, new RankTracker()); +const INIT_STATE = { + gameId: await gameId(genId(0)), + rank: "B-", + rankPoint: 100, +}; + +class TestRankTracker extends RankTracker { + testGet() { + const { state, deltaMap } = this; + return { + state, + deltaMap, + }; + } +} + +function genId(id: number): string { + return base64.encode( + `VsHistoryDetail-asdf:asdf:20220101T${ + id.toString().padStart(6, "0") + }_------------------------------------`, + ); +} + +function genOpenWins( + { startId, count, udemae }: { + startId: number; + count: number; + udemae: string; + }, +) { + const result: BattleListNode[] = []; + let id = startId; + + for (let i = 0; i < count; i++) { + result.push({ + id: genId(id), + udemae, + judgement: "WIN", + bankaraMatch: { + earnedUdemaePoint: 8, + }, + nextHistoryDetail: null, + previousHistoryDetail: null, + }); + id += 1; + } + + return result; +} + +Deno.test("RankTracker", async () => { + const tracker = new TestRankTracker(INIT_STATE); + assertEquals(tracker.testGet(), { + state: INIT_STATE, + deltaMap: new Map(), + }); + + const finalState = await tracker.updateState([{ + bankaraMatchChallenge: null, + historyDetails: { + nodes: [...genOpenWins({ + startId: 0, + count: 19, + udemae: "B-", + })].reverse(), + }, + }]); + + assertEquals(tracker.testGet().state, INIT_STATE); + + assertEquals(finalState, { + gameId: await gameId(genId(18)), + rank: "B-", + rankPoint: 244, + }); + + assertEquals(await tracker.getRankStateById(genId(1)), { + gameId: await gameId(genId(1)), + rank: "B-", + rankPoint: 108, + }); + + assertEquals(await tracker.getRankStateById(genId(17)), { + gameId: await gameId(genId(17)), + rank: "B-", + rankPoint: 236, + }); + + tracker.setState(finalState); + + assertEquals(tracker.testGet().state, finalState); + + // history goes too far + const finalState2 = await tracker.updateState([{ + bankaraMatchChallenge: null, + historyDetails: { + nodes: [...genOpenWins({ + startId: 30, + count: 1, + udemae: "B-", + })].reverse(), + }, + }]); + assertEquals(finalState2, undefined); + + await tracker.updateState([{ + bankaraMatchChallenge: null, + historyDetails: { + nodes: [...genOpenWins({ + startId: 0, + count: 30, + udemae: "B-", + })].reverse(), + }, + }]); + + assertEquals(await tracker.getRankStateById(genId(29)), { + gameId: await gameId(genId(29)), + rank: "B-", + rankPoint: 332, + }); }); diff --git a/src/RankTracker.ts b/src/RankTracker.ts index f537d8c..9b7ed95 100644 --- a/src/RankTracker.ts +++ b/src/RankTracker.ts @@ -143,9 +143,9 @@ const battleTime = (id: string) => { */ export class RankTracker { // key: privous game id - deltaMap: Map = new Map(); + protected deltaMap: Map = new Map(); - constructor(private state: RankState | undefined) {} + constructor(protected state: RankState | undefined) {} async getRankStateById(id: string): Promise { if (!this.state) { From 8180de928cb809825c260a6086dcd48737db450c Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Sat, 29 Oct 2022 16:34:50 +0800 Subject: [PATCH 05/12] feat: add rankAfter in Delta --- src/RankTracker.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/RankTracker.ts b/src/RankTracker.ts index 9b7ed95..6591872 100644 --- a/src/RankTracker.ts +++ b/src/RankTracker.ts @@ -76,6 +76,7 @@ export const RANK_PARAMS: RankParam[] = [{ type Delta = { beforeGameId: string; gameId: string; + rankAfter?: string; rankPoint: number; isRankUp: boolean; isChallengeFirst: boolean; @@ -83,7 +84,7 @@ type Delta = { function addRank(state: RankState, delta: Delta): RankState { const { rank, rankPoint } = state; - const { gameId, isRankUp, isChallengeFirst } = delta; + const { gameId, rankAfter, isRankUp, isChallengeFirst } = delta; const rankIndex = RANK_PARAMS.findIndex((r) => r.rank === rank); @@ -122,7 +123,7 @@ function addRank(state: RankState, delta: Delta): RankState { return { gameId, - rank, + rank: rankAfter ?? rank, rankPoint: rankPoint + delta.rankPoint, }; } @@ -149,7 +150,7 @@ export class RankTracker { async getRankStateById(id: string): Promise { if (!this.state) { - return undefined; + return; } const gid = await gameId(id); @@ -157,7 +158,7 @@ export class RankTracker { while (cur.gameId !== gid) { const delta = this.deltaMap.get(cur.gameId); if (!delta) { - throw new Error("Delta not found"); + return; } cur = addRank(cur, delta); } @@ -217,12 +218,13 @@ export class RankTracker { isChallengeFirst: false, }; beforeGameId = i.gameId; - // challenge 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, isRankUp: i.bankaraMatchChallenge.isUdemaeUp ?? false, isChallengeFirst: i.index === 0, @@ -235,8 +237,11 @@ export class RankTracker { }; } } else { + // open delta = { ...delta, + // TODO: is this right? + rankAfter: i.detail.udemae, rankPoint: i.detail.bankaraMatch?.earnedUdemaePoint, }; } From d2d04c703d64cee8ea9aa635ea28a061fd8a457b Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Sat, 29 Oct 2022 16:51:55 +0800 Subject: [PATCH 06/12] feat: upload rank point to stat.ink --- src/GameFetcher.ts | 8 ++++++-- src/RankTracker.ts | 14 +++++++++++--- src/exporters/stat.ink.ts | 23 +++++++++++++++++++++-- src/types.ts | 1 + 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/GameFetcher.ts b/src/GameFetcher.ts index 693b80c..e20ab83 100644 --- a/src/GameFetcher.ts +++ b/src/GameFetcher.ts @@ -62,7 +62,7 @@ export class GameFetcher { return finalState; } - getRankStateById(id: string): Promise { + getRankStateById(id: string) { return this.rankTracker.getRankStateById(id); } @@ -140,6 +140,7 @@ export class GameFetcher { bankaraMatchChallenge: null, listNode: null, rankState: null, + rankBeforeState: null, }; } @@ -164,12 +165,15 @@ export class GameFetcher { }; } + const { before, after } = await this.rankTracker.getRankStateById(id) ?? {}; + return { type: "VsInfo", bankaraMatchChallenge, listNode, challengeProgress, - rankState: await this.rankTracker.getRankStateById(id) ?? null, + rankState: after ?? null, + rankBeforeState: before ?? null, }; } cacheDetail( diff --git a/src/RankTracker.ts b/src/RankTracker.ts index 6591872..a5a3801 100644 --- a/src/RankTracker.ts +++ b/src/RankTracker.ts @@ -82,6 +82,7 @@ type Delta = { 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, isRankUp, isChallengeFirst } = delta; @@ -148,22 +149,29 @@ export class RankTracker { constructor(protected state: RankState | undefined) {} - async getRankStateById(id: string): Promise { + async getRankStateById( + id: string, + ): Promise<{ before: RankState; after: RankState } | undefined> { if (!this.state) { return; } const gid = await gameId(id); let cur = this.state; + let before = cur; while (cur.gameId !== gid) { const delta = this.deltaMap.get(cur.gameId); if (!delta) { return; } + before = cur; cur = addRank(cur, delta); } - return cur; + return { + before, + after: cur, + }; } setState(state: RankState | undefined) { @@ -240,7 +248,7 @@ export class RankTracker { // open delta = { ...delta, - // TODO: is this right? + // TODO: rankAfter should be undefined in open battle rankAfter: i.detail.udemae, rankPoint: i.detail.bankaraMatch?.earnedUdemaePoint, }; diff --git a/src/exporters/stat.ink.ts b/src/exporters/stat.ink.ts index 69e7509..d0d17b5 100644 --- a/src/exporters/stat.ink.ts +++ b/src/exporters/stat.ink.ts @@ -173,8 +173,14 @@ export class StatInkExporter implements GameExporter { return result; } async mapBattle( - { challengeProgress, bankaraMatchChallenge, listNode, detail: vsDetail }: - VsInfo, + { + challengeProgress, + bankaraMatchChallenge, + listNode, + detail: vsDetail, + rankBeforeState, + rankState, + }: VsInfo, ): Promise { const { knockout, @@ -276,6 +282,19 @@ export class StatInkExporter implements GameExporter { result.challenge_lose = challengeProgress.loseCount; } + if (rankBeforeState) { + result.rank_before_exp = rankBeforeState.rankPoint; + } + + if (rankState) { + result.rank_after_exp = rankState.rankPoint; + if (!result.rank_after) { + [result.rank_after, result.rank_after_s_plus] = parseUdemae( + rankState.rank, + ); + } + } + return result; } } diff --git a/src/types.ts b/src/types.ts index 0702591..e3a5959 100644 --- a/src/types.ts +++ b/src/types.ts @@ -115,6 +115,7 @@ export type VsInfo = { bankaraMatchChallenge: null | BankaraMatchChallenge; challengeProgress: null | ChallengeProgress; rankState: null | RankState; + rankBeforeState: null | RankState; detail: VsHistoryDetail; }; // Salmon run From 705d4dc7f9f1bf63bafa55f0f412c022db8a116b Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Sat, 29 Oct 2022 17:03:14 +0800 Subject: [PATCH 07/12] test: add charge test --- src/RankTracker.test.ts | 59 ++++++++++++++++++++++++++++++++++++++--- src/RankTracker.ts | 2 +- src/types.ts | 2 +- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/RankTracker.test.ts b/src/RankTracker.test.ts index 91c23c4..406302b 100644 --- a/src/RankTracker.test.ts +++ b/src/RankTracker.test.ts @@ -55,6 +55,59 @@ function genOpenWins( return result; } +Deno.test("RankTracker tracks challenge charge", async () => { + const tracker = new TestRankTracker(INIT_STATE); + assertEquals(tracker.testGet(), { + state: INIT_STATE, + deltaMap: new Map(), + }); + + const finalState = await tracker.updateState([{ + bankaraMatchChallenge: { + winCount: 1, + loseCount: 0, + maxWinCount: 5, + maxLoseCount: 3, + isPromo: false, + isUdemaeUp: false, + udemaeAfter: null, + earnedUdemaePoint: null, + state: "INPROGRESS", + }, + historyDetails: { + nodes: [ + { + id: genId(2), + udemae: "B-", + judgement: "WIN", + bankaraMatch: { + earnedUdemaePoint: null, + }, + nextHistoryDetail: null, + previousHistoryDetail: null, + }, + ], + }, + }, { + bankaraMatchChallenge: null, + historyDetails: { + nodes: genOpenWins({ + startId: 0, + count: 1, + udemae: "B-", + }), + }, + }]); + + assertEquals(tracker.testGet().state, INIT_STATE); + + assertEquals(finalState, { + gameId: await gameId(genId(2)), + rank: "B-", + rankPoint: 45, + }); +}); + Deno.test("RankTracker", async () => { const tracker = new TestRankTracker(INIT_STATE); assertEquals(tracker.testGet(), { @@ -81,13 +134,13 @@ Deno.test("RankTracker", async () => { rankPoint: 244, }); - assertEquals(await tracker.getRankStateById(genId(1)), { + assertEquals((await tracker.getRankStateById(genId(1)))?.after, { gameId: await gameId(genId(1)), rank: "B-", rankPoint: 108, }); - assertEquals(await tracker.getRankStateById(genId(17)), { + assertEquals((await tracker.getRankStateById(genId(17)))?.after, { gameId: await gameId(genId(17)), rank: "B-", rankPoint: 236, @@ -121,7 +174,7 @@ Deno.test("RankTracker", async () => { }, }]); - assertEquals(await tracker.getRankStateById(genId(29)), { + assertEquals((await tracker.getRankStateById(genId(29)))?.after, { gameId: await gameId(genId(29)), rank: "B-", rankPoint: 332, diff --git a/src/RankTracker.ts b/src/RankTracker.ts index a5a3801..d2def0b 100644 --- a/src/RankTracker.ts +++ b/src/RankTracker.ts @@ -250,7 +250,7 @@ export class RankTracker { ...delta, // TODO: rankAfter should be undefined in open battle rankAfter: i.detail.udemae, - rankPoint: i.detail.bankaraMatch?.earnedUdemaePoint, + rankPoint: i.detail.bankaraMatch?.earnedUdemaePoint ?? 0, }; } diff --git a/src/types.ts b/src/types.ts index e3a5959..ef168e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,7 +52,7 @@ export type BattleListNode = { id: string; }; bankaraMatch: null | { - earnedUdemaePoint: number; + earnedUdemaePoint: null | number; }; }; export type CoopListNode = { From b2b997ef653b75f130de68a6311bbcbf74d14333 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Sat, 29 Oct 2022 17:23:40 +0800 Subject: [PATCH 08/12] fix: isChallengeFirst --- src/RankTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RankTracker.ts b/src/RankTracker.ts index d2def0b..0c698f3 100644 --- a/src/RankTracker.ts +++ b/src/RankTracker.ts @@ -235,7 +235,7 @@ export class RankTracker { rankAfter: i.bankaraMatchChallenge.udemaeAfter ?? undefined, rankPoint: i.bankaraMatchChallenge.earnedUdemaePoint ?? 0, isRankUp: i.bankaraMatchChallenge.isUdemaeUp ?? false, - isChallengeFirst: i.index === 0, + isChallengeFirst: false, }; } else if (i.index === i.groupLength - 1) { // first battle in challenge From 0a10bdf470ff8979a61098bfb2cdfb3a02477095 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Sat, 29 Oct 2022 17:26:38 +0800 Subject: [PATCH 09/12] fix: promotion challenge --- src/RankTracker.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/RankTracker.ts b/src/RankTracker.ts index 0c698f3..747ffb7 100644 --- a/src/RankTracker.ts +++ b/src/RankTracker.ts @@ -78,14 +78,14 @@ type Delta = { gameId: string; rankAfter?: string; rankPoint: number; - isRankUp: boolean; + isPromotion: 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, isRankUp, isChallengeFirst } = delta; + const { gameId, rankAfter, isPromotion, isChallengeFirst } = delta; const rankIndex = RANK_PARAMS.findIndex((r) => r.rank === rank); @@ -112,7 +112,7 @@ function addRank(state: RankState, delta: Delta): RankState { }; } - if (isRankUp) { + if (isPromotion) { const nextRankParam = RANK_PARAMS[rankIndex + 1]; return { @@ -222,7 +222,7 @@ export class RankTracker { beforeGameId, gameId: i.gameId, rankPoint: 0, - isRankUp: false, + isPromotion: false, isChallengeFirst: false, }; beforeGameId = i.gameId; @@ -234,7 +234,7 @@ export class RankTracker { ...delta, rankAfter: i.bankaraMatchChallenge.udemaeAfter ?? undefined, rankPoint: i.bankaraMatchChallenge.earnedUdemaePoint ?? 0, - isRankUp: i.bankaraMatchChallenge.isUdemaeUp ?? false, + isPromotion: i.bankaraMatchChallenge.isPromo ?? false, isChallengeFirst: false, }; } else if (i.index === i.groupLength - 1) { From e12f6c1c51b8a1f9d1ffbc950eac3a842eef0bd9 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Sun, 30 Oct 2022 21:27:55 +0800 Subject: [PATCH 10/12] build: update README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 641a3d3..a59a50e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,18 @@ Options: - If you want to use a different profile, use `-p` to specify the path to the profile file. +### Track your rank + +- Run + `deno run -Ar https://raw.githubusercontent.com/spacemeowx2/s3si.ts/main/initRank.ts` + to initialize your rank data. (You can also use `-p` to specify the path to + the profile file.) + +- Then enter your current rank and rank point. For example: `S+0,300`. And the + rank will be saved in the `profile.json`. + +- After that, run `s3si.ts`, the rank point will be reported to `stat.ink`. + ### profile.json ```js From 2e748adcf40e852e2b2c15baa6c73264fbc0a5f6 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Sun, 30 Oct 2022 21:33:10 +0800 Subject: [PATCH 11/12] build: bump version, update CHANGELOG --- CHANGELOG | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index f4561be..5b01b20 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,33 @@ +0.1.12 + +feat: add rank tracker + +0.1.11 + +feat: use s3s' namespace. (see https://github.com/frozenpandaman/s3s/issues/65 for detail) +refactor: remove `_bid` in exported file + +0.1.10 + +fix: missing draw judgement + +0.1.9 + +update WEB_VIEW_VERSION + +0.1.8 + +feat: add coop export + +0.1.7 + +feat: refetch token when 401 close #5 +feat: add github link to UA + +0.1.6 + +fix: wrong base64 encode/decode. (#4) + 0.1.5 fix: rank_up sent on last battle of challenge From dc69b3dc5caffaa609ab36a7a99895e63a06b4b0 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Mon, 31 Oct 2022 13:31:30 +0800 Subject: [PATCH 12/12] test: add promotion test --- src/RankTracker.test.ts | 113 ++++++++++++++++++++++++++++++++++++++-- src/types.ts | 6 --- 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/RankTracker.test.ts b/src/RankTracker.test.ts index 406302b..36b32e7 100644 --- a/src/RankTracker.test.ts +++ b/src/RankTracker.test.ts @@ -46,8 +46,6 @@ function genOpenWins( bankaraMatch: { earnedUdemaePoint: 8, }, - nextHistoryDetail: null, - previousHistoryDetail: null, }); id += 1; } @@ -55,6 +53,115 @@ function genOpenWins( return result; } +Deno.test("RankTracker tracks promotion, ignoring INPROGRESS", async () => { + const INIT_STATE = { + gameId: await gameId(genId(0)), + rank: "B+", + rankPoint: 850, + }; + + const tracker = new TestRankTracker(INIT_STATE); + assertEquals(tracker.testGet(), { + state: INIT_STATE, + deltaMap: new Map(), + }); + + const finalState = await tracker.updateState([{ + bankaraMatchChallenge: { + winCount: 2, + loseCount: 0, + maxWinCount: 3, + maxLoseCount: 3, + state: "INPROGRESS", + isPromo: true, + isUdemaeUp: true, + udemaeAfter: "A-", + earnedUdemaePoint: null, + }, + historyDetails: { + nodes: [{ + id: await genId(1), + udemae: "B+", + judgement: "WIN", + bankaraMatch: { + earnedUdemaePoint: null, + }, + }, { + id: await genId(0), + udemae: "B+", + judgement: "WIN", + bankaraMatch: { + earnedUdemaePoint: null, + }, + }], + }, + }]); + + assertEquals(finalState, { + gameId: await gameId(genId(1)), + rank: "B+", + rankPoint: 850, + }); +}); + +Deno.test("RankTracker tracks promotion", async () => { + const INIT_STATE = { + gameId: await gameId(genId(0)), + rank: "B+", + rankPoint: 850, + }; + + const tracker = new TestRankTracker(INIT_STATE); + assertEquals(tracker.testGet(), { + state: INIT_STATE, + 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: await genId(2), + udemae: "B+", + judgement: "WIN", + bankaraMatch: { + earnedUdemaePoint: null, + }, + }, { + id: await genId(1), + udemae: "B+", + judgement: "WIN", + bankaraMatch: { + earnedUdemaePoint: null, + }, + }, { + id: await genId(0), + udemae: "B+", + judgement: "WIN", + bankaraMatch: { + earnedUdemaePoint: null, + }, + }], + }, + }]); + + assertEquals(finalState, { + gameId: await gameId(genId(2)), + rank: "A-", + rankPoint: 200, + }); +}); + Deno.test("RankTracker tracks challenge charge", async () => { const tracker = new TestRankTracker(INIT_STATE); assertEquals(tracker.testGet(), { @@ -83,8 +190,6 @@ Deno.test("RankTracker tracks challenge charge", async () => { bankaraMatch: { earnedUdemaePoint: null, }, - nextHistoryDetail: null, - previousHistoryDetail: null, }, ], }, diff --git a/src/types.ts b/src/types.ts index ef168e6..236cddf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,12 +45,6 @@ export type BattleListNode = { id: string; udemae: string; judgement: "LOSE" | "WIN" | "DEEMED_LOSE" | "EXEMPTED_LOSE" | "DRAW"; - nextHistoryDetail: null | { - id: string; - }; - previousHistoryDetail: null | { - id: string; - }; bankaraMatch: null | { earnedUdemaePoint: null | number; };