mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 21:55:15 -05:00
Migrate Node -> Bun (#1827)
* Initial * Faster user page * Remove redundant function * Favorite badge sorting * Upgrade deps * Simplify entry.server * Bun tests initial * Update package.json npm -> bun * Update README * Type safe translations again * Don't load streams info for finalized tournaments * Translations as an object * More unit test work * Convert match.server.test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * Test & all done * Working cf * Bun GA try * No cache * spacing * spacing 2 * Add SQL logging * Remove NR * Hmm * Hmm 2 * Interesting * SKALOP_SYSTEM_MESSAGE_URL * . * . * ? * . * ? * Server.ts adjust * Downgrade Tldraw * E2E test fix * Fix lint
This commit is contained in:
@@ -27,3 +27,6 @@ SKALOP_TOKEN=secret
|
||||
|
||||
VITE_SITE_DOMAIN=http://localhost:5173
|
||||
VITE_SKALOP_WS_URL=ws://localhost:5900
|
||||
|
||||
// trunc, full or none (default: none)
|
||||
SQL_LOG=trunc
|
||||
|
||||
8
.env.test
Normal file
8
.env.test
Normal file
@@ -0,0 +1,8 @@
|
||||
DB_PATH=db-test-active.sqlite3
|
||||
|
||||
SQL_LOG=none
|
||||
|
||||
BASE_URL=https://example.com
|
||||
|
||||
SKALOP_SYSTEM_MESSAGE_URL=http://skalop.test
|
||||
SKALOP_TOKEN=test
|
||||
24
.github/workflows/main.yml
vendored
24
.github/workflows/main.yml
vendored
@@ -11,25 +11,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: npm-
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Formatter/Linter
|
||||
run: npm run biome:check
|
||||
run: bun run biome:check
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
run: bun run typecheck
|
||||
- name: Unit tests
|
||||
run: bun run test:unit:all
|
||||
- name: Check translations jsons
|
||||
run: npm run check-translation-jsons:no-write
|
||||
run: bun run check-translation-jsons:no-write
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,6 +23,4 @@ dump
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
newrelic_agent.log
|
||||
|
||||
.vscode
|
||||
|
||||
42
README.md
42
README.md
@@ -40,9 +40,10 @@ Competitive Splatoon Platform
|
||||
- React
|
||||
- Remix
|
||||
- Sqlite3
|
||||
- Bun
|
||||
- CSS (plain)
|
||||
- E2E tests via Playwright
|
||||
- Unit/integration tests via uvu
|
||||
- Unit/integration tests via bun:test
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -61,11 +62,12 @@ Prerequisites: [nvm](https://github.com/nvm-sh/nvm)
|
||||
There is a sequence of commands you need to run:
|
||||
|
||||
1. `nvm use` to switch to the correct Node version. If you don't have the correct Node.js version yet it will prompt you to install it via the `nvm install` command. If you have problems with nvm you can also install the latest LTS version of Node.js from [their website](https://nodejs.org/en/).
|
||||
2. `npm i` to install the dependencies.
|
||||
3. Make a copy of `.env.example` that's called `.env`. Filling additional values is not necessary unless you want to use real Discord authentication or develop the Lohi bot.
|
||||
4. `npm run migrate up` to set up the database tables.
|
||||
5. `npm run dev` to run the project in development mode.
|
||||
6. Navigate to `http://localhost:5173/admin`. There press the seed button to fill the DB with test data. You can also impersonate any user (Sendou#0043 = admin).
|
||||
2. Install latest version of [Bun](https://bun.sh/docs/installation)
|
||||
3. `bun install` to install the dependencies.
|
||||
4. Make a copy of `.env.example` that's called `.env`. Filling additional values is not necessary unless you want to use real Discord authentication or develop the Lohi bot.
|
||||
5. `bun migrate up` to set up the database tables.
|
||||
6. `bun run dev` to run the project in development mode.
|
||||
7. Navigate to `http://localhost:5173/admin`. There press the seed button to fill the DB with test data. You can also impersonate any user (Sendou#0043 = admin).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -114,6 +116,12 @@ Any questions please ask Sendou!
|
||||
7. Send the file to Sendou (or open a pull request if you know how)
|
||||
8. Optional: also send an image as .png if you want to show a link preview. The preferred dimensions are 1200 × 630.
|
||||
|
||||
## SQL Logging
|
||||
|
||||
By default SQL is logged in truncated format. You can adjust this by changing the `SQL_LOG` env var. Possible values are "trunc", "full" and "none".
|
||||
|
||||
Note it only logs queries made via Kysely.
|
||||
|
||||
## API
|
||||
|
||||
If you want to use the API then please leave an issue explaining your use case. By default, I want to allow open use of the data on the site. It's just not recommended to use the same APIs the web pages use as they are not stable at all and can change at any time without warning.
|
||||
@@ -160,25 +168,25 @@ Some common files:
|
||||
### Update friend code
|
||||
|
||||
```bash
|
||||
npx tsx scripts/update-fc.ts 79237403620945920 1234-1234-1234
|
||||
bun scripts/update-fc.ts 79237403620945920 1234-1234-1234
|
||||
```
|
||||
|
||||
### Add new badge to the database
|
||||
|
||||
```bash
|
||||
npx tsx scripts/add-badge.ts fire_green "Octofin Eliteboard"
|
||||
bun scripts/add-badge.ts fire_green "Octofin Eliteboard"
|
||||
```
|
||||
|
||||
### Rename display name of a badge
|
||||
|
||||
```bash
|
||||
npx tsx scripts/rename-badge.ts 10 "New 4v4 Sundaes"
|
||||
bun scripts/rename-badge.ts 10 "New 4v4 Sundaes"
|
||||
```
|
||||
|
||||
### Add many badge owners
|
||||
|
||||
```bash
|
||||
npx tsx scripts/add-badge-winners.ts 10 "750705955909664791,79237403620945920"
|
||||
bun scripts/add-badge-winners.ts 10 "750705955909664791,79237403620945920"
|
||||
```
|
||||
|
||||
### Converting gifs (badges) to thumbnail (.png)
|
||||
@@ -220,19 +228,19 @@ Note: This is only useful if you have access to a production running on Render.c
|
||||
4. Update `CURRENT_PATCH` constants
|
||||
5. Update `PATCHES` constant with the late patch + remove the oldest
|
||||
6. Update the stage list in `stage-ids.ts` and `create-misc-json.ts`. Add images from Lean's repository and avify them.
|
||||
7. `npx tsx scripts/create-misc-json.ts`
|
||||
8. `npx tsx scripts/create-gear-json.ts`
|
||||
9. `npx tsx scripts/create-analyzer-json.ts`
|
||||
7. `bun scripts/create-misc-json.ts`
|
||||
8. `bun scripts/create-gear-json.ts`
|
||||
9. `bun scripts/create-analyzer-json.ts`
|
||||
8a. Double check that no hard-coded special damages changed
|
||||
10. `npx tsx scripts/create-object-dmg-json.ts`
|
||||
10. `bun scripts/create-object-dmg-json.ts`
|
||||
11. Fill new weapon IDs by category to `weapon-ids.ts` (easy to take from the diff of English weapons.json)
|
||||
12. Get gear IDs for each slot from /output folder and update `gear-ids.ts`.
|
||||
13. Replace `object-dmg.json` with the `object-dmg.json` in /output folder
|
||||
14. Replace `weapon-params.ts` with the `params.json` in /output folder
|
||||
15. Delete all images inside `main-weapons`, `main-weapons-outlined`, `main-weapons-outlined-2` and `gear` folders.
|
||||
16. Replace with images from Lean's repository.
|
||||
17. Run the `npx tsx scripts/replace-img-names.ts` command
|
||||
18. Run the `npx tsx scripts/replace-weapon-names.ts` command
|
||||
17. Run the `bun scripts/replace-img-names.ts` command
|
||||
18. Run the `bun scripts/replace-weapon-names.ts` command
|
||||
19. Run the .avif generating command in each image folder.
|
||||
20. Update manually any languages that use English `gear.json` and `weapons.json` files
|
||||
|
||||
@@ -241,7 +249,7 @@ Note: This is only useful if you have access to a production running on Render.c
|
||||
If you change any files and the CI pipeline errors out on certain formatting/linting steps (Biome) run this command in the repo's root directory:
|
||||
|
||||
```sh
|
||||
npm run cf
|
||||
bun cf
|
||||
```
|
||||
|
||||
Before committing, if for some reason you see an abnormally high amount of files changed, simply run `git add --renormalize .` and it will fix the error.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useActionData } from "@remix-run/react";
|
||||
import type { CustomTypeOptions } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Namespace } from "~/modules/i18n/resources.server";
|
||||
|
||||
export function FormErrors({
|
||||
namespace,
|
||||
}: {
|
||||
namespace: keyof CustomTypeOptions["resources"];
|
||||
namespace: Namespace;
|
||||
}) {
|
||||
const { t } = useTranslation(["common", namespace]);
|
||||
const actionData = useActionData<{ errors?: string[] }>();
|
||||
|
||||
@@ -725,7 +725,7 @@ function calendarEvents() {
|
||||
description: faker.lorem.paragraph(),
|
||||
discordInviteCode: faker.lorem.word(),
|
||||
bracketUrl: faker.internet.url(),
|
||||
authorId: id === 1 ? NZAP_TEST_ID : userIds.pop(),
|
||||
authorId: id === 1 ? NZAP_TEST_ID : userIds.pop() ?? null,
|
||||
tags:
|
||||
Math.random() > 0.2
|
||||
? shuffledTags
|
||||
@@ -1835,7 +1835,7 @@ function arts() {
|
||||
return faker.image.url();
|
||||
}
|
||||
|
||||
return urls.pop();
|
||||
return urls.pop() ?? null;
|
||||
};
|
||||
|
||||
const addedArt = addArtStm.get({
|
||||
@@ -1859,7 +1859,7 @@ function arts() {
|
||||
) {
|
||||
addArtUserMetadataStm.run({
|
||||
artId: addedArt.id,
|
||||
userId: i === 0 ? NZAP_TEST_ID : allUsers.pop(),
|
||||
userId: i === 0 ? NZAP_TEST_ID : allUsers.pop() ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2026,7 +2026,6 @@ async function playedMatches() {
|
||||
|
||||
invariant(groupAlpha !== 0 && groupBravo !== 0, "groups not created");
|
||||
|
||||
// @ts-expect-error creating without memento on purpose
|
||||
const match = createMatch({
|
||||
alphaGroupId: groupAlpha,
|
||||
bravoGroupId: groupBravo,
|
||||
|
||||
@@ -1,32 +1,87 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { styleText } from "node:util";
|
||||
import { Kysely, type LogEvent, ParseJSONResultsPlugin } from "kysely";
|
||||
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
||||
import { format } from "sql-formatter";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
import type { DB } from "./tables";
|
||||
|
||||
const migratedEmptyDb = new Database("db-test.sqlite3").serialize();
|
||||
|
||||
invariant(process.env.DB_PATH, "DB_PATH env variable must be set");
|
||||
const isInMemoryDB = process.env.DB_PATH === ":memory:";
|
||||
|
||||
export const sql = new Database(
|
||||
isInMemoryDB ? migratedEmptyDb : process.env.DB_PATH,
|
||||
const LOG_LEVEL = (["trunc", "full", "none"] as const).find(
|
||||
(val) => val === process.env.SQL_LOG,
|
||||
);
|
||||
|
||||
sql.pragma("journal_mode = WAL");
|
||||
sql.pragma("foreign_keys = ON");
|
||||
sql.pragma("busy_timeout = 5000");
|
||||
invariant(process.env.DB_PATH, "DB_PATH env variable must be set");
|
||||
|
||||
export const sql = new Database(process.env.DB_PATH, {
|
||||
strict: true,
|
||||
});
|
||||
|
||||
sql.exec("PRAGMA journal_mode = WAL;");
|
||||
sql.exec("PRAGMA foreign_keys = ON;");
|
||||
sql.exec("PRAGMA busy_timeout = 5000;");
|
||||
|
||||
export const db = new Kysely<DB>({
|
||||
dialect: new SqliteDialect({
|
||||
dialect: new BunSqliteDialect({
|
||||
database: sql,
|
||||
}),
|
||||
// uncomment if you want examine the queries
|
||||
// log: process.env.NODE_ENV === "development" ? ["query"] : undefined,
|
||||
// log(event): void {
|
||||
// if (event.level === "query") {
|
||||
// console.log(event.query.sql);
|
||||
// console.log(event.query.parameters);
|
||||
// }
|
||||
// },
|
||||
log: LOG_LEVEL === "trunc" || LOG_LEVEL === "full" ? logQuery : undefined,
|
||||
plugins: [new ParseJSONResultsPlugin()],
|
||||
});
|
||||
|
||||
function logQuery(event: LogEvent) {
|
||||
const isSelectQuery = Boolean((event.query.query as any).from?.froms);
|
||||
|
||||
if (event.level === "query" && isSelectQuery) {
|
||||
const from = () =>
|
||||
(event.query.query as any).from.froms.map(
|
||||
(f: any) => f.table.identifier.name,
|
||||
);
|
||||
// biome-ignore lint/suspicious/noConsoleLog: dev only
|
||||
console.log(styleText("blue", `-- SQLITE QUERY to "${from()}" --`));
|
||||
// biome-ignore lint/suspicious/noConsoleLog: dev only
|
||||
console.log(
|
||||
styleText(
|
||||
millisToColor(event.queryDurationMillis),
|
||||
`${roundToNDecimalPlaces(event.queryDurationMillis, 1)}ms`,
|
||||
),
|
||||
);
|
||||
// biome-ignore lint/suspicious/noConsoleLog: dev only
|
||||
console.log(formatSql(event.query.sql, event.query.parameters));
|
||||
}
|
||||
}
|
||||
|
||||
function millisToColor(millis: number) {
|
||||
if (millis < 1) {
|
||||
return "bgGreen";
|
||||
}
|
||||
if (millis < 5) {
|
||||
return "green";
|
||||
}
|
||||
if (millis < 50) {
|
||||
return "yellow";
|
||||
}
|
||||
return "red";
|
||||
}
|
||||
|
||||
function formatSql(sql: string, params: readonly unknown[]) {
|
||||
const formatted = format(sql);
|
||||
|
||||
const lines = formatted.split("\n");
|
||||
|
||||
if (LOG_LEVEL === "full" || lines.length <= 11) {
|
||||
return addParams(formatted, params);
|
||||
}
|
||||
|
||||
const linesNotShown = lines.length - 10;
|
||||
|
||||
return `${lines.slice(0, 10).join("\n")}\n... (${linesNotShown} more lines) ...\n`;
|
||||
}
|
||||
|
||||
function addParams(sql: string, params: readonly unknown[]) {
|
||||
const coloredParams = params.map((param) =>
|
||||
styleText("yellow", JSON.stringify(param)),
|
||||
);
|
||||
|
||||
return sql.replace(/\?/g, () => coloredParams.shift() || "");
|
||||
}
|
||||
|
||||
@@ -1,167 +1,79 @@
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
import {
|
||||
type ActionFunctionArgs,
|
||||
type EntryContext,
|
||||
type LoaderFunctionArgs,
|
||||
createReadableStreamFromReadable,
|
||||
} from "@remix-run/node";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { createInstance } from "i18next";
|
||||
import { isbot } from "isbot";
|
||||
import cron from "node-cron";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { getUser } from "./features/auth/core/user.server";
|
||||
import { i18Instance } from "./modules/i18n/loader.server";
|
||||
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||
import { config } from "~/modules/i18n/config"; // your i18n configuration file
|
||||
import i18next from "~/modules/i18n/i18next.server";
|
||||
import { resources } from "./modules/i18n/resources.server";
|
||||
import { updatePatreonData } from "./modules/patreon";
|
||||
import { noticeError, setTransactionName } from "./utils/newrelic.server";
|
||||
|
||||
const ABORT_DELAY = 5000;
|
||||
|
||||
const handleRequest = (
|
||||
export default async function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
) => {
|
||||
const userAgent = request.headers.get("user-agent");
|
||||
|
||||
const lastMatch =
|
||||
remixContext.staticHandlerContext.matches[
|
||||
remixContext.staticHandlerContext.matches.length - 1
|
||||
];
|
||||
|
||||
if (lastMatch) setTransactionName(`ssr/${lastMatch.route.id}`);
|
||||
|
||||
return userAgent && isbot(userAgent)
|
||||
? handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext,
|
||||
)
|
||||
: handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext,
|
||||
);
|
||||
};
|
||||
export default handleRequest;
|
||||
|
||||
export function handleDataRequest(
|
||||
response: Response,
|
||||
{ request }: LoaderFunctionArgs | ActionFunctionArgs,
|
||||
) {
|
||||
const name = new URL(request.url).searchParams.get("_data");
|
||||
if (name) setTransactionName(name);
|
||||
const callbackName = isbot(request.headers.get("user-agent"))
|
||||
? "onAllReady"
|
||||
: "onShellReady";
|
||||
|
||||
return response;
|
||||
}
|
||||
const instance = createInstance();
|
||||
const lng = await i18next.getLocale(request);
|
||||
const ns = i18next.getRouteNamespaces(remixContext);
|
||||
|
||||
const handleBotRequest = (
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
await instance
|
||||
.use(initReactI18next) // Tell our instance to use react-i18next
|
||||
.init({
|
||||
...config, // spread the configuration
|
||||
lng, // The locale we detected above
|
||||
ns, // The namespaces the routes about to render wants to use
|
||||
resources,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
|
||||
void i18Instance(request, remixContext).then((i18n) => {
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
</I18nextProvider>,
|
||||
{
|
||||
onAllReady: () => {
|
||||
const body = new PassThrough();
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<I18nextProvider i18n={instance}>
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
</I18nextProvider>,
|
||||
{
|
||||
[callbackName]: () => {
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
}),
|
||||
);
|
||||
|
||||
resolve(
|
||||
new Response(createReadableStreamFromReadable(body), {
|
||||
headers: responseHeaders,
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
}),
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError: (error: unknown) => {
|
||||
reject(error);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
didError = true;
|
||||
|
||||
console.error(error);
|
||||
},
|
||||
pipe(body);
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
});
|
||||
|
||||
const handleBrowserRequest = (
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
|
||||
void i18Instance(request, remixContext).then((i18n) => {
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
</I18nextProvider>,
|
||||
{
|
||||
onShellReady: () => {
|
||||
const body = new PassThrough();
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
resolve(
|
||||
new Response(createReadableStreamFromReadable(body), {
|
||||
headers: responseHeaders,
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
}),
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError: (error: unknown) => {
|
||||
reject(error);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
didError = true;
|
||||
|
||||
console.error(error);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
onError(error: unknown) {
|
||||
didError = true;
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
console.error(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
|
||||
export async function handleError(
|
||||
error: unknown,
|
||||
{ request }: LoaderFunctionArgs | ActionFunctionArgs,
|
||||
) {
|
||||
const user = await getUser(request);
|
||||
if (!request.signal.aborted) {
|
||||
if (error instanceof Error) {
|
||||
noticeError(error, {
|
||||
"enduser.id": user?.id,
|
||||
// TODO: FetchError: Invalid response body while trying to fetch http://localhost:5800/admin?_data=features%2Fadmin%2Froutes%2Fadmin: This stream has already been locked for exclusive reading by another reader
|
||||
// formData: JSON.stringify(formDataToObject(await request.formData())),
|
||||
});
|
||||
}
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// example from https://github.com/BenMcH/remix-rss/blob/main/app/entry.server.tsx
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import MockDate from "mockdate";
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { afterEach, describe, expect, setSystemTime, test } from "bun:test";
|
||||
import { db } from "~/db/sql";
|
||||
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
|
||||
import * as Test from "~/utils/Test";
|
||||
import { dbInsertUsers, dbReset, wrappedAction } from "~/utils/Test";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import type { adminActionSchema } from "../actions/admin.server";
|
||||
import { action } from "./admin";
|
||||
|
||||
const PlusVoting = suite("Plus voting");
|
||||
|
||||
const adminAction = Test.wrappedAction<typeof adminActionSchema>({ action });
|
||||
const adminAction = wrappedAction<typeof adminActionSchema>({ action });
|
||||
|
||||
const voteArgs = ({
|
||||
score,
|
||||
@@ -57,75 +53,74 @@ const createLeaderboard = (userIds: number[]) =>
|
||||
)
|
||||
.execute();
|
||||
|
||||
PlusVoting.after.each(() => {
|
||||
MockDate.reset();
|
||||
Test.database.reset();
|
||||
});
|
||||
describe("Plus voting", () => {
|
||||
afterEach(() => {
|
||||
setSystemTime();
|
||||
dbReset();
|
||||
});
|
||||
|
||||
PlusVoting("gives correct amount of plus tiers", async () => {
|
||||
MockDate.set(new Date("2023-12-12T00:00:00.000Z"));
|
||||
test("gives correct amount of plus tiers", async () => {
|
||||
setSystemTime(new Date("2023-12-12T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(10);
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
const id = i + 1;
|
||||
await dbInsertUsers(10);
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
const id = i + 1;
|
||||
|
||||
return voteArgs({
|
||||
score: id <= 5 ? -1 : 1,
|
||||
votedId: id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
return voteArgs({
|
||||
score: id <= 5 ? -1 : 1,
|
||||
votedId: id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.equal(await countPlusTierMembers(), 5);
|
||||
});
|
||||
expect(await countPlusTierMembers()).toBe(5);
|
||||
});
|
||||
|
||||
PlusVoting("60% is the criteria to pass voting", async () => {
|
||||
MockDate.set(new Date("2023-12-12T00:00:00.000Z"));
|
||||
test("60% is the criteria to pass voting", async () => {
|
||||
setSystemTime(new Date("2023-12-12T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(10);
|
||||
await dbInsertUsers(10);
|
||||
|
||||
// 50%
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
authorId: i + 1,
|
||||
score: i < 5 ? -1 : 1,
|
||||
votedId: 1,
|
||||
});
|
||||
}),
|
||||
);
|
||||
// 60%
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
authorId: i + 1,
|
||||
score: i < 4 ? -1 : 1,
|
||||
votedId: 2,
|
||||
});
|
||||
}),
|
||||
);
|
||||
// 50%
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
authorId: i + 1,
|
||||
score: i < 5 ? -1 : 1,
|
||||
votedId: 1,
|
||||
});
|
||||
}),
|
||||
);
|
||||
// 60%
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
authorId: i + 1,
|
||||
score: i < 4 ? -1 : 1,
|
||||
votedId: 2,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
const rows = await db
|
||||
.selectFrom("PlusTier")
|
||||
.select(["PlusTier.tier", "PlusTier.userId"])
|
||||
.where("PlusTier.tier", "=", 1)
|
||||
.execute();
|
||||
const rows = await db
|
||||
.selectFrom("PlusTier")
|
||||
.select(["PlusTier.tier", "PlusTier.userId"])
|
||||
.where("PlusTier.tier", "=", 1)
|
||||
.execute();
|
||||
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].userId, 2);
|
||||
});
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].userId).toBe(2);
|
||||
});
|
||||
|
||||
PlusVoting(
|
||||
"combines leaderboard and voting results (after season over)",
|
||||
async () => {
|
||||
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
|
||||
test("combines leaderboard and voting results (after season over)", async () => {
|
||||
setSystemTime(new Date("2023-11-29T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(2);
|
||||
await dbInsertUsers(2);
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: 1,
|
||||
@@ -136,16 +131,13 @@ PlusVoting(
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.equal(await countPlusTierMembers(), 2);
|
||||
},
|
||||
);
|
||||
expect(await countPlusTierMembers()).toBe(2);
|
||||
});
|
||||
|
||||
PlusVoting(
|
||||
"skips users from leaderboard with the skip flag for the season",
|
||||
async () => {
|
||||
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
|
||||
test("skips users from leaderboard with the skip flag for the season", async () => {
|
||||
setSystemTime(new Date("2023-11-29T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(11);
|
||||
await dbInsertUsers(11);
|
||||
await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1));
|
||||
|
||||
await db
|
||||
@@ -156,66 +148,63 @@ PlusVoting(
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.equal(await countPlusTierMembers(1), 10);
|
||||
assert.equal(await countPlusTierMembers(2), 0);
|
||||
},
|
||||
);
|
||||
expect(await countPlusTierMembers(1)).toBe(10);
|
||||
expect(await countPlusTierMembers(2)).toBe(0);
|
||||
});
|
||||
|
||||
PlusVoting("plus server skip flag ignored if for past season", async () => {
|
||||
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
|
||||
test("plus server skip flag ignored if for past season", async () => {
|
||||
setSystemTime(new Date("2023-11-29T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(11);
|
||||
await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1));
|
||||
await dbInsertUsers(11);
|
||||
await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1));
|
||||
|
||||
await db
|
||||
.updateTable("User")
|
||||
.set({ plusSkippedForSeasonNth: 0 })
|
||||
.where("User.id", "=", 1)
|
||||
.execute();
|
||||
await db
|
||||
.updateTable("User")
|
||||
.set({ plusSkippedForSeasonNth: 0 })
|
||||
.where("User.id", "=", 1)
|
||||
.execute();
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.equal(await countPlusTierMembers(1), 10);
|
||||
assert.equal(await countPlusTierMembers(2), 1);
|
||||
});
|
||||
expect(await countPlusTierMembers(1)).toBe(10);
|
||||
expect(await countPlusTierMembers(2)).toBe(1);
|
||||
});
|
||||
|
||||
PlusVoting("ignores leaderboard while season is ongoing", async () => {
|
||||
MockDate.set(new Date("2024-02-15T00:00:00.000Z"));
|
||||
test("ignores leaderboard while season is ongoing", async () => {
|
||||
setSystemTime(new Date("2024-02-15T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(2);
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: 1,
|
||||
votedId: 1,
|
||||
}),
|
||||
]);
|
||||
await createLeaderboard([2]);
|
||||
await dbInsertUsers(2);
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: 1,
|
||||
votedId: 1,
|
||||
}),
|
||||
]);
|
||||
await createLeaderboard([2]);
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.equal(await countPlusTierMembers(), 1);
|
||||
assert.equal(await countPlusTierMembers(2), 0);
|
||||
});
|
||||
expect(await countPlusTierMembers()).toBe(1);
|
||||
expect(await countPlusTierMembers(2)).toBe(0);
|
||||
});
|
||||
|
||||
PlusVoting("leaderboard gives members to all tiers", async () => {
|
||||
MockDate.set(new Date("2023-11-20T00:00:00.000Z"));
|
||||
test("leaderboard gives members to all tiers", async () => {
|
||||
setSystemTime(new Date("2023-11-20T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(60);
|
||||
await createLeaderboard(Array.from({ length: 60 }, (_, i) => i + 1));
|
||||
await dbInsertUsers(60);
|
||||
await createLeaderboard(Array.from({ length: 60 }, (_, i) => i + 1));
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.ok((await countPlusTierMembers()) > 0);
|
||||
assert.ok((await countPlusTierMembers(2)) > 0);
|
||||
assert.ok((await countPlusTierMembers(3)) > 0);
|
||||
});
|
||||
expect(await countPlusTierMembers()).toBeGreaterThan(0);
|
||||
expect(await countPlusTierMembers(2)).toBeGreaterThan(0);
|
||||
expect(await countPlusTierMembers(3)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
PlusVoting(
|
||||
"gives membership if failed voting and is on the leaderboard",
|
||||
async () => {
|
||||
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
|
||||
test("gives membership if failed voting and is on the leaderboard", async () => {
|
||||
setSystemTime(new Date("2023-11-29T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(1);
|
||||
await dbInsertUsers(1);
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: -1,
|
||||
@@ -226,35 +215,33 @@ PlusVoting(
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.equal(await countPlusTierMembers(1), 1);
|
||||
},
|
||||
);
|
||||
expect(await countPlusTierMembers(1)).toBe(1);
|
||||
});
|
||||
|
||||
PlusVoting("members who fails voting drops one tier", async () => {
|
||||
MockDate.set(new Date("2024-02-15T00:00:00.000Z"));
|
||||
test("members who fails voting drops one tier", async () => {
|
||||
setSystemTime(new Date("2024-02-15T00:00:00.000Z"));
|
||||
|
||||
await Test.database.insertUsers(1);
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: 1,
|
||||
votedId: 1,
|
||||
month: 11,
|
||||
year: 2023,
|
||||
}),
|
||||
]);
|
||||
await dbInsertUsers(1);
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: 1,
|
||||
votedId: 1,
|
||||
month: 11,
|
||||
year: 2023,
|
||||
}),
|
||||
]);
|
||||
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: -1,
|
||||
votedId: 1,
|
||||
month: 2,
|
||||
year: 2024,
|
||||
}),
|
||||
]);
|
||||
await PlusVotingRepository.upsertMany([
|
||||
voteArgs({
|
||||
score: -1,
|
||||
votedId: 1,
|
||||
month: 2,
|
||||
year: 2024,
|
||||
}),
|
||||
]);
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
assert.equal(await countPlusTierMembers(2), 1);
|
||||
expect(await countPlusTierMembers(2)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
PlusVoting.run();
|
||||
|
||||
@@ -99,8 +99,16 @@ type AddNewArtArgs = Pick<Art, "authorId" | "description"> &
|
||||
};
|
||||
|
||||
export const addNewArt = sql.transaction((args: AddNewArtArgs) => {
|
||||
const img = addImgStm.get(args) as UserSubmittedImage;
|
||||
const art = addArtStm.get({ ...args, imgId: img.id }) as Art;
|
||||
const img = addImgStm.get({
|
||||
authorId: args.authorId,
|
||||
url: args.url,
|
||||
validatedAt: args.validatedAt,
|
||||
}) as UserSubmittedImage;
|
||||
const art = addArtStm.get({
|
||||
authorId: args.authorId,
|
||||
description: args.description,
|
||||
imgId: img.id,
|
||||
}) as Art;
|
||||
|
||||
for (const userId of args.linkedUsers) {
|
||||
addArtUserMetadataStm.run({ artId: art.id, userId });
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { sql } from "~/db/sql";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
select
|
||||
count(distinct "Art"."id") as "count"
|
||||
from
|
||||
"Art"
|
||||
left join "ArtUserMetadata" on "ArtUserMetadata"."artId" = "Art"."id"
|
||||
inner join "UserSubmittedImage" on "UserSubmittedImage"."id" = "Art"."imgId"
|
||||
where "Art"."authorId" = @userId
|
||||
or "ArtUserMetadata"."userId" = @userId
|
||||
`);
|
||||
|
||||
export function countArtByUserId(userId: number) {
|
||||
return stm.pluck().get({ userId }) as number;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export class DiscordStrategy extends OAuth2Strategy<
|
||||
discordUserDetailsSchema.parse(discordResponses);
|
||||
|
||||
const isAlreadyRegistered = Boolean(
|
||||
await UserRepository.findByIdentifier(user.id),
|
||||
await UserRepository.identifierToUserId(user.id),
|
||||
);
|
||||
|
||||
if (!isAlreadyRegistered && !user.verified) {
|
||||
|
||||
@@ -37,9 +37,8 @@ const addXPBadgeStm = sql.prepare(/* sql */ `
|
||||
|
||||
export const syncXPBadges = sql.transaction(() => {
|
||||
for (const value of SPLATOON_3_XP_BADGE_VALUES) {
|
||||
const badgeId = badgeCodeToIdStm
|
||||
.pluck()
|
||||
.get({ code: String(value) }) as number;
|
||||
const badgeId = (badgeCodeToIdStm.get({ code: String(value) }) as any)
|
||||
.id as number;
|
||||
|
||||
invariant(badgeId, `Badge ${value} not found`);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export const loader = async () => {
|
||||
};
|
||||
|
||||
export default function BadgesPageLayout() {
|
||||
const { t } = useTranslation("badges");
|
||||
const { t } = useTranslation(["badges"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const user = useUser();
|
||||
const [inputValue, setInputValue] = React.useState("");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type {
|
||||
AbilityWithUnknown,
|
||||
BuildAbilitiesTupleWithUnknown,
|
||||
@@ -24,147 +23,146 @@ function validateAbilityChunksArray(
|
||||
expectedOutput,
|
||||
)}\nActual Output: ${JSON.stringify(abilityChunksArray)}`;
|
||||
|
||||
assert.ok(isFoundInAbilityChunksArray, errorString);
|
||||
expect(isFoundInAbilityChunksArray, errorString).toBeTruthy();
|
||||
}
|
||||
}
|
||||
|
||||
const GetAbilityChunksMapAsArray = suite("getAbilityChunksMapAsArray()");
|
||||
describe("getAbilityChunksMapAsArray()", () => {
|
||||
test("Empty build results in an empty array", () => {
|
||||
const emptyBuild = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
GetAbilityChunksMapAsArray("Empty build results in an empty array", () => {
|
||||
const emptyBuild = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(emptyBuild);
|
||||
expect(
|
||||
abilityChunksArray,
|
||||
"Ability chunks array is not empty.",
|
||||
).toBeEmpty();
|
||||
});
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(emptyBuild);
|
||||
assert.equal(abilityChunksArray, [], "Ability chunks array is not empty.");
|
||||
describe("getAbilityChunksMapAsArray()", () => {
|
||||
test("Empty build results in an empty array", () => {
|
||||
const emptyBuild = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(emptyBuild);
|
||||
expect(
|
||||
abilityChunksArray,
|
||||
"Ability chunks array is not empty.",
|
||||
).toBeEmpty();
|
||||
});
|
||||
|
||||
test("Ability Doubler ability does not count towards Ability Chunks", () => {
|
||||
const buildWithOnlyAbilityDoubler = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["AD", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(
|
||||
buildWithOnlyAbilityDoubler,
|
||||
);
|
||||
expect(abilityChunksArray).toEqual([]);
|
||||
});
|
||||
|
||||
test("Main Ability stackable ability chunk calculation is correct", () => {
|
||||
const build = [
|
||||
["ISS", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["ISM", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput: any = [
|
||||
["ISM", 45],
|
||||
["ISS", 45],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(build);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
});
|
||||
|
||||
test("Ninja Squid ability chunk calculation is correct (for a primary slot-only ability)", () => {
|
||||
const build = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["NS", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput: any = [
|
||||
["IRU", 15],
|
||||
["RSU", 15],
|
||||
["SSU", 15],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(build);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
});
|
||||
|
||||
test("Ability chunk calculation is correct for a real build. Each gear has 1, 2 or 3 ability chunks of same type", () => {
|
||||
const slayerBuild = [
|
||||
["LDE", "SSU", "SSU", "SSU"],
|
||||
["NS", "QR", "QR", "ISM"],
|
||||
["SJ", "SSU", "RES", "QSJ"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput: any = [
|
||||
["SSU", 85],
|
||||
["IRU", 30],
|
||||
["QR", 30],
|
||||
["ISM", 25],
|
||||
["QSJ", 25],
|
||||
["IA", 15],
|
||||
["ISS", 15],
|
||||
["RSU", 15],
|
||||
["SRU", 15],
|
||||
["RES", 10],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(slayerBuild);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
});
|
||||
|
||||
test("Ability chunk calculation is correct for a real build (Splatling)", () => {
|
||||
const splatlingBuild = [
|
||||
["RSU", "QSJ", "SSU", "RSU"],
|
||||
["RSU", "ISM", "ISM", "RSU"],
|
||||
["OS", "SSU", "SSU", "RES"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput: any = [
|
||||
["RSU", 110],
|
||||
["SSU", 40],
|
||||
["ISM", 30],
|
||||
["BRU", 15],
|
||||
["IRU", 15],
|
||||
["SPU", 15],
|
||||
["QSJ", 10],
|
||||
["RES", 10],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(splatlingBuild);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
});
|
||||
|
||||
test("Sub abilities chunk calculation with Ability Doubler in Clothing slot is correct", () => {
|
||||
const build = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["AD", "SSU", "SSU", "ISM"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput: any = [
|
||||
["SSU", 9],
|
||||
["ISM", 3],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(build);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
GetAbilityChunksMapAsArray(
|
||||
"Ability Doubler ability does not count towards Ability Chunks",
|
||||
() => {
|
||||
const buildWithOnlyAbilityDoubler = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["AD", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(
|
||||
buildWithOnlyAbilityDoubler,
|
||||
);
|
||||
assert.equal(abilityChunksArray, [], "Ability chunks array is not empty.");
|
||||
},
|
||||
);
|
||||
|
||||
GetAbilityChunksMapAsArray(
|
||||
"Main Ability stackable ability chunk calculation is correct",
|
||||
() => {
|
||||
const build = [
|
||||
["ISS", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["ISM", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput = [
|
||||
["ISM", 45],
|
||||
["ISS", 45],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(build);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
},
|
||||
);
|
||||
|
||||
GetAbilityChunksMapAsArray(
|
||||
"Ninja Squid ability chunk calculation is correct (for a primary slot-only ability)",
|
||||
() => {
|
||||
const build = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["NS", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput = [
|
||||
["RSU", 15],
|
||||
["IRU", 15],
|
||||
["SSU", 15],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(build);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
},
|
||||
);
|
||||
|
||||
GetAbilityChunksMapAsArray(
|
||||
"Ability chunk calculation is correct for a real build. Each gear has 1, 2 or 3 ability chunks of same type",
|
||||
() => {
|
||||
const slayerBuild = [
|
||||
["LDE", "SSU", "SSU", "SSU"],
|
||||
["NS", "QR", "QR", "ISM"],
|
||||
["SJ", "SSU", "RES", "QSJ"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput = [
|
||||
["SSU", 85],
|
||||
["IRU", 30],
|
||||
["QR", 30],
|
||||
["ISM", 25],
|
||||
["QSJ", 25],
|
||||
["IA", 15],
|
||||
["ISS", 15],
|
||||
["RSU", 15],
|
||||
["SRU", 15],
|
||||
["RES", 10],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(slayerBuild);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
},
|
||||
);
|
||||
|
||||
GetAbilityChunksMapAsArray(
|
||||
"Ability chunk calculation is correct for a real build (Splatling)",
|
||||
() => {
|
||||
const splatlingBuild = [
|
||||
["RSU", "QSJ", "SSU", "RSU"],
|
||||
["RSU", "ISM", "ISM", "RSU"],
|
||||
["OS", "SSU", "SSU", "RES"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput = [
|
||||
["RSU", 110],
|
||||
["SSU", 40],
|
||||
["ISM", 30],
|
||||
["BRU", 15],
|
||||
["IRU", 15],
|
||||
["SPU", 15],
|
||||
["QSJ", 10],
|
||||
["RES", 10],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(splatlingBuild);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
},
|
||||
);
|
||||
|
||||
GetAbilityChunksMapAsArray(
|
||||
"Sub abilities chunk calculation with Ability Doubler in Clothing slot is correct",
|
||||
() => {
|
||||
const build = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["AD", "SSU", "SSU", "ISM"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
] as unknown as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
const expectedOutput = [
|
||||
["SSU", 9],
|
||||
["ISM", 3],
|
||||
];
|
||||
|
||||
const abilityChunksArray = getAbilityChunksMapAsArray(build);
|
||||
validateAbilityChunksArray(abilityChunksArray, expectedOutput);
|
||||
},
|
||||
);
|
||||
|
||||
GetAbilityChunksMapAsArray.run();
|
||||
|
||||
@@ -1,95 +1,86 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { applySpecialEffects } from "./specialEffects";
|
||||
|
||||
const ApplySpecialEffects = suite("applySpecialEffects()");
|
||||
describe("applySpecialEffects()", () => {
|
||||
test("Adds an effect to empty build", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map(),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Adds an effect to empty build", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map(),
|
||||
ldeIntensity: 0,
|
||||
expect(aps.size).toBe(6);
|
||||
expect(aps.get("ISM")).toBe(10);
|
||||
});
|
||||
|
||||
assert.equal(aps.size, 6);
|
||||
assert.equal(aps.get("ISM"), 10);
|
||||
});
|
||||
|
||||
ApplySpecialEffects(
|
||||
"Adds an effect to build while keeping existing abilities untouched",
|
||||
() => {
|
||||
test("Adds an effect to build while keeping existing abilities untouched", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map([["SPU", 10]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.size, 7);
|
||||
assert.equal(aps.get("SPU"), 10);
|
||||
},
|
||||
);
|
||||
|
||||
ApplySpecialEffects("Does not boost ability beyond 57", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map([["ISM", 57]]),
|
||||
ldeIntensity: 0,
|
||||
expect(aps.size).toBe(7);
|
||||
expect(aps.get("SPU")).toBe(10);
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM"), 57);
|
||||
});
|
||||
test("Does not boost ability beyond 57", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map([["ISM", 57]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Tacticooler doesn't boost swim speed beyond 29", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["SSU", 28]]),
|
||||
ldeIntensity: 0,
|
||||
expect(aps.get("ISM")).toBe(57);
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU"), 29);
|
||||
});
|
||||
test("Tacticooler doesn't boost swim speed beyond 29", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["SSU", 28]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
ApplySpecialEffects(
|
||||
"Tacticooler limit swim speed at 29 if more in build",
|
||||
() => {
|
||||
expect(aps.get("SSU")).toBe(29);
|
||||
});
|
||||
|
||||
test("Tacticooler limit swim speed at 29 if more in build", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["SSU", 30]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU"), 30);
|
||||
},
|
||||
);
|
||||
|
||||
ApplySpecialEffects("Applies many effects", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["DR", "CB"],
|
||||
abilityPoints: new Map([["SSU", 1]]),
|
||||
ldeIntensity: 0,
|
||||
expect(aps.get("SSU")).toBe(30);
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU"), 41);
|
||||
});
|
||||
test("Applies many effects", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["DR", "CB"],
|
||||
abilityPoints: new Map([["SSU", 1]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Applies LDE", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 1,
|
||||
expect(aps.get("SSU")).toBe(41);
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM"), 1);
|
||||
});
|
||||
test("Applies LDE", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 1,
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Applies LDE (intensity != aps given)", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 15,
|
||||
expect(aps.get("ISM")).toBe(1);
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM"), 13);
|
||||
});
|
||||
test("Applies LDE (intensity != aps given)", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 15,
|
||||
});
|
||||
|
||||
ApplySpecialEffects.run();
|
||||
expect(aps.get("ISM")).toBe(13);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,86 +1,79 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type MainWeaponId, mainWeaponIds } from "~/modules/in-game-lists";
|
||||
import { damageTypeToWeaponType } from "../analyzer-constants";
|
||||
import { buildStats } from "./stats";
|
||||
|
||||
const AnalyzeBuild = suite("Analyze build");
|
||||
describe("Analyze build", () => {
|
||||
test("Every main weapon has damage", () => {
|
||||
const weaponsWithoutDamage: MainWeaponId[] = [];
|
||||
|
||||
AnalyzeBuild("Every main weapon has damage", () => {
|
||||
const weaponsWithoutDamage: MainWeaponId[] = [];
|
||||
for (const weaponSplId of mainWeaponIds) {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
for (const weaponSplId of mainWeaponIds) {
|
||||
const hasDamage =
|
||||
analyzed.stats.damages.filter(
|
||||
(dmg) => damageTypeToWeaponType[dmg.type] === "MAIN",
|
||||
).length > 0;
|
||||
|
||||
if (!hasDamage) {
|
||||
weaponsWithoutDamage.push(weaponSplId);
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
weaponsWithoutDamage.length,
|
||||
`Weapons without damage set: ${weaponsWithoutDamage.join(", ")}`,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test("Ninja Squid decreases swim speed", () => {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId,
|
||||
weaponSplId: 0,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
const hasDamage =
|
||||
analyzed.stats.damages.filter(
|
||||
(dmg) => damageTypeToWeaponType[dmg.type] === "MAIN",
|
||||
).length > 0;
|
||||
const analyzedWithNS = buildStats({
|
||||
weaponSplId: 0,
|
||||
mainOnlyAbilities: ["NS"],
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
if (!hasDamage) {
|
||||
weaponsWithoutDamage.push(weaponSplId);
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
weaponsWithoutDamage.length === 0,
|
||||
`Weapons without damage set: ${weaponsWithoutDamage.join(", ")}`,
|
||||
);
|
||||
});
|
||||
|
||||
AnalyzeBuild("Ninja Squid decreases swim speed", () => {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: 0,
|
||||
hasTacticooler: false,
|
||||
expect(analyzed.stats.swimSpeed.value).toBeGreaterThan(
|
||||
analyzedWithNS.stats.swimSpeed.value,
|
||||
);
|
||||
});
|
||||
|
||||
const analyzedWithNS = buildStats({
|
||||
weaponSplId: 0,
|
||||
mainOnlyAbilities: ["NS"],
|
||||
hasTacticooler: false,
|
||||
});
|
||||
test("Tacticooler / RP calculated correctly", () => {
|
||||
const fullQR = buildStats({
|
||||
weaponSplId: 0,
|
||||
abilityPoints: new Map([["QR", 57]]),
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
analyzed.stats.swimSpeed.value > analyzedWithNS.stats.swimSpeed.value,
|
||||
);
|
||||
});
|
||||
const tacticooler = buildStats({
|
||||
weaponSplId: 0,
|
||||
abilityPoints: new Map([["QR", 57]]),
|
||||
hasTacticooler: true,
|
||||
});
|
||||
|
||||
AnalyzeBuild("Tacticooler / RP calculated correctly", () => {
|
||||
const fullQR = buildStats({
|
||||
weaponSplId: 0,
|
||||
abilityPoints: new Map([["QR", 57]]),
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
const tacticooler = buildStats({
|
||||
weaponSplId: 0,
|
||||
abilityPoints: new Map([["QR", 57]]),
|
||||
hasTacticooler: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
fullQR.stats.quickRespawnTime.value ===
|
||||
expect(
|
||||
fullQR.stats.quickRespawnTime.value,
|
||||
"Base QR should be same whether 57AP of QR or Tacticooler",
|
||||
).toBe(tacticooler.stats.quickRespawnTime.value);
|
||||
expect(
|
||||
fullQR.stats.quickRespawnTimeSplattedByRP.value,
|
||||
"Tacticooler splatted by RP should respawn faster than 57AP of QR",
|
||||
).toBeGreaterThan(tacticooler.stats.quickRespawnTimeSplattedByRP.value);
|
||||
expect(
|
||||
tacticooler.stats.quickRespawnTime.value,
|
||||
"Base QR should be same whether 57AP of QR or Tacticooler",
|
||||
);
|
||||
assert.ok(
|
||||
fullQR.stats.quickRespawnTimeSplattedByRP.value >
|
||||
tacticooler.stats.quickRespawnTimeSplattedByRP.value,
|
||||
"Tacticooler splatted by RP should respawn faster than 57AP of QR",
|
||||
);
|
||||
assert.ok(
|
||||
tacticooler.stats.quickRespawnTime.value <
|
||||
tacticooler.stats.quickRespawnTimeSplattedByRP.value,
|
||||
"Tacticooler should respawn faster than Tacticooler splatted by RP",
|
||||
);
|
||||
});
|
||||
"Tacticooler should respawn faster than Tacticooler splatted by RP",
|
||||
).toBeLessThan(tacticooler.stats.quickRespawnTimeSplattedByRP.value);
|
||||
});
|
||||
|
||||
AnalyzeBuild(
|
||||
"Accounts for Jr. big ink tank with sub weapon ink consumption %",
|
||||
() => {
|
||||
test("Accounts for Jr. big ink tank with sub weapon ink consumption %", () => {
|
||||
const analyzedDualieSquelchers = buildStats({
|
||||
weaponSplId: 5030,
|
||||
hasTacticooler: false,
|
||||
@@ -91,37 +84,34 @@ AnalyzeBuild(
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
analyzedDualieSquelchers.stats.subWeaponInkConsumptionPercentage.value >
|
||||
analyzedJr.stats.subWeaponInkConsumptionPercentage.value,
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(
|
||||
analyzedDualieSquelchers.stats.subWeaponInkConsumptionPercentage.value,
|
||||
).toBeGreaterThan(analyzedJr.stats.subWeaponInkConsumptionPercentage.value);
|
||||
});
|
||||
|
||||
const subPowerApToQuickSuperJumpAp = new Map([
|
||||
[0, 0],
|
||||
[3, 4],
|
||||
[6, 9],
|
||||
[13, 18],
|
||||
[28, 36],
|
||||
[57, 57],
|
||||
]);
|
||||
const subPowerApToQuickSuperJumpAp = new Map([
|
||||
[0, 0],
|
||||
[3, 4],
|
||||
[6, 9],
|
||||
[13, 18],
|
||||
[28, 36],
|
||||
[57, 57],
|
||||
]);
|
||||
|
||||
AnalyzeBuild("Sub Power Up Beakon AP boost matches Lean", () => {
|
||||
for (const [subPowerAp, quickSuperJumpAp] of subPowerApToQuickSuperJumpAp) {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: 1011,
|
||||
abilityPoints: new Map([["BRU" as const, subPowerAp]]),
|
||||
hasTacticooler: false,
|
||||
});
|
||||
test("Sub Power Up Beakon AP boost matches Lean", () => {
|
||||
for (const [subPowerAp, quickSuperJumpAp] of subPowerApToQuickSuperJumpAp) {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: 1011,
|
||||
abilityPoints: new Map([["BRU" as const, subPowerAp]]),
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
analyzed.stats.subQsjBoost?.value === quickSuperJumpAp,
|
||||
`Wrong AP boost for ${subPowerAp}AP of Sub Power Up: ${
|
||||
analyzed.stats.subQsjBoost!.value
|
||||
} (expected ${quickSuperJumpAp}))`,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
analyzed.stats.subQsjBoost?.value,
|
||||
`Wrong AP boost for ${subPowerAp}AP of Sub Power Up: ${
|
||||
analyzed.stats.subQsjBoost!.value
|
||||
} (expected ${quickSuperJumpAp}))`,
|
||||
).toBe(quickSuperJumpAp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AnalyzeBuild.run();
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { AbilityWithUnknown } from "~/modules/in-game-lists/types";
|
||||
import { buildToAbilityPoints } from "./utils";
|
||||
|
||||
const BuildToAbilityPoints = suite("buildToAbilityPoints()");
|
||||
describe("buildToAbilityPoints", () => {
|
||||
const EMPTY_ROW: [
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
] = ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"];
|
||||
|
||||
const EMPTY_ROW: [
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
] = ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"];
|
||||
test("calculates ability points correctly", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
["SS", "SS", "RSU", "RSU"],
|
||||
EMPTY_ROW,
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
BuildToAbilityPoints("Calculates ability points", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
["SS", "SS", "RSU", "RSU"],
|
||||
EMPTY_ROW,
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
expect(aps.get("SS")).toBe(13);
|
||||
expect(aps.get("RSU")).toBe(6);
|
||||
expect(aps.get("UNKNOWN")).toBe(38);
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SS"), 13);
|
||||
assert.equal(aps.get("RSU"), 6);
|
||||
assert.equal(aps.get("UNKNOWN"), 38);
|
||||
test("handles ability doubler correctly", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
EMPTY_ROW,
|
||||
["AD", "SS", "UNKNOWN", "UNKNOWN"],
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
expect(aps.get("SS")).toBe(6);
|
||||
});
|
||||
|
||||
test("does not calculate AP for main only abilities", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
["LDE", "SS", "RSU", "RSU"],
|
||||
EMPTY_ROW,
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
expect(aps.has("LDE")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Handles ability doubler", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
EMPTY_ROW,
|
||||
["AD", "SS", "UNKNOWN", "UNKNOWN"],
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
assert.equal(aps.get("SS"), 6);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Does not calculate AP for main only abilities", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
["LDE", "SS", "RSU", "RSU"],
|
||||
EMPTY_ROW,
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
assert.not.ok(aps.has("LDE"));
|
||||
});
|
||||
|
||||
BuildToAbilityPoints.run();
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
abilityPointCountsToAverages,
|
||||
popularBuilds,
|
||||
} from "./build-stats-utils";
|
||||
|
||||
const AbilityPointCountsToAverages = suite("abilityPointCountsToAverages()");
|
||||
const PopularBuilds = suite("popularBuilds()");
|
||||
|
||||
const commonAbilities = [
|
||||
{
|
||||
ability: "QR" as const,
|
||||
@@ -36,115 +32,113 @@ const allAbilities = [
|
||||
{ ability: "BRU" as const, abilityPointsSum: 57 },
|
||||
];
|
||||
|
||||
AbilityPointCountsToAverages("calculates build count", () => {
|
||||
const { weaponBuildsCount } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
describe("abilityPointCountsToAverages", () => {
|
||||
test("calculates build count", () => {
|
||||
const { weaponBuildsCount } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
});
|
||||
|
||||
expect(weaponBuildsCount).toBe(2);
|
||||
});
|
||||
|
||||
assert.is(weaponBuildsCount, 2);
|
||||
});
|
||||
test("calculates average ap (main only)", () => {
|
||||
const { mainOnlyAbilities } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
});
|
||||
|
||||
AbilityPointCountsToAverages("calculates average ap (main only)", () => {
|
||||
const { mainOnlyAbilities } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
expect(
|
||||
mainOnlyAbilities.find((a) => a.name === "T")?.percentage.weapon,
|
||||
).toBe(50);
|
||||
});
|
||||
|
||||
assert.is(
|
||||
mainOnlyAbilities.find((a) => a.name === "T")?.percentage.weapon,
|
||||
50,
|
||||
);
|
||||
});
|
||||
test("calculates average ap (stackable)", () => {
|
||||
const { stackableAbilities } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
});
|
||||
|
||||
AbilityPointCountsToAverages("calculates average ap (stackable)", () => {
|
||||
const { stackableAbilities } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
expect(
|
||||
stackableAbilities.find((a) => a.name === "SS")?.apAverage.weapon,
|
||||
).toBe(13.5);
|
||||
});
|
||||
|
||||
assert.is(
|
||||
stackableAbilities.find((a) => a.name === "SS")?.apAverage.weapon,
|
||||
13.5,
|
||||
);
|
||||
test("calculates average ap for all builds", () => {
|
||||
const { mainOnlyAbilities } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
});
|
||||
|
||||
expect(mainOnlyAbilities.find((a) => a.name === "T")?.percentage.all).toBe(
|
||||
33.33,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
AbilityPointCountsToAverages("calculates average ap for all builds", () => {
|
||||
const { mainOnlyAbilities } = abilityPointCountsToAverages({
|
||||
allAbilities,
|
||||
weaponAbilities: commonAbilities,
|
||||
describe("popularBuilds", () => {
|
||||
test("calculates popular build", () => {
|
||||
const builds = popularBuilds([
|
||||
...new Array(10).fill(null).map(() => ({
|
||||
abilities: [{ ability: "QR" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
{
|
||||
abilities: [{ ability: "BRU" as const, abilityPoints: 57 }],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(builds.length).toBe(1);
|
||||
expect(builds[0].count).toBe(10);
|
||||
expect(builds[0].abilities[0].ability).toBe("QR");
|
||||
});
|
||||
|
||||
assert.is(
|
||||
mainOnlyAbilities.find((a) => a.name === "T")?.percentage.all,
|
||||
33.33,
|
||||
);
|
||||
test("calculates second most popular build (sorted by count)", () => {
|
||||
const builds = popularBuilds([
|
||||
...new Array(10).fill(null).map(() => ({
|
||||
abilities: [{ ability: "QR" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
...new Array(3).fill(null).map(() => ({
|
||||
abilities: [{ ability: "SS" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
...new Array(5).fill(null).map(() => ({
|
||||
abilities: [{ ability: "SSU" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
]);
|
||||
|
||||
expect(builds.length).toBe(3);
|
||||
expect(builds[1].abilities[0].ability).toBe("SSU");
|
||||
});
|
||||
|
||||
test("sums up abilities", () => {
|
||||
const builds = popularBuilds([
|
||||
{ abilities: [{ ability: "QR" as const, abilityPoints: 57 }] },
|
||||
{
|
||||
abilities: [
|
||||
{ ability: "QR" as const, abilityPoints: 10 },
|
||||
{ ability: "QR" as const, abilityPoints: 47 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(builds.length).toBe(1);
|
||||
});
|
||||
|
||||
test("sorts abilities", () => {
|
||||
const builds = popularBuilds([
|
||||
{
|
||||
abilities: [
|
||||
{ ability: "QR" as const, abilityPoints: 10 },
|
||||
{ ability: "SS" as const, abilityPoints: 47 },
|
||||
],
|
||||
},
|
||||
{
|
||||
abilities: [
|
||||
{ ability: "QR" as const, abilityPoints: 10 },
|
||||
{ ability: "SS" as const, abilityPoints: 47 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(builds[0].abilities[1].ability).toBe("QR");
|
||||
});
|
||||
});
|
||||
|
||||
PopularBuilds("calculates popular build", () => {
|
||||
const builds = popularBuilds([
|
||||
...new Array(10).fill(null).map(() => ({
|
||||
abilities: [{ ability: "QR" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
{
|
||||
abilities: [{ ability: "BRU" as const, abilityPoints: 57 }],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.is(builds.length, 1);
|
||||
assert.is(builds[0].count, 10);
|
||||
assert.is(builds[0].abilities[0].ability, "QR");
|
||||
});
|
||||
|
||||
PopularBuilds("calculates second most popular build (sorted by count)", () => {
|
||||
const builds = popularBuilds([
|
||||
...new Array(10).fill(null).map(() => ({
|
||||
abilities: [{ ability: "QR" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
...new Array(3).fill(null).map(() => ({
|
||||
abilities: [{ ability: "SS" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
...new Array(5).fill(null).map(() => ({
|
||||
abilities: [{ ability: "SSU" as const, abilityPoints: 57 }],
|
||||
})),
|
||||
]);
|
||||
|
||||
assert.is(builds.length, 3);
|
||||
assert.is(builds[1].abilities[0].ability, "SSU");
|
||||
});
|
||||
|
||||
PopularBuilds("sums up abilities", () => {
|
||||
const builds = popularBuilds([
|
||||
{ abilities: [{ ability: "QR" as const, abilityPoints: 57 }] },
|
||||
{
|
||||
abilities: [
|
||||
{ ability: "QR" as const, abilityPoints: 10 },
|
||||
{ ability: "QR" as const, abilityPoints: 47 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.is(builds.length, 1);
|
||||
});
|
||||
|
||||
PopularBuilds("sorts abilities", () => {
|
||||
const builds = popularBuilds([
|
||||
{
|
||||
abilities: [
|
||||
{ ability: "QR" as const, abilityPoints: 10 },
|
||||
{ ability: "SS" as const, abilityPoints: 47 },
|
||||
],
|
||||
},
|
||||
{
|
||||
abilities: [
|
||||
{ ability: "QR" as const, abilityPoints: 10 },
|
||||
{ ability: "SS" as const, abilityPoints: 47 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.is(builds[0].abilities[1].ability, "QR");
|
||||
});
|
||||
|
||||
AbilityPointCountsToAverages.run();
|
||||
PopularBuilds.run();
|
||||
|
||||
@@ -25,5 +25,7 @@ export interface AverageAbilityPointsResult {
|
||||
export function averageAbilityPoints(weaponSplId?: MainWeaponId | null) {
|
||||
const stm = typeof weaponSplId === "number" ? findByWeaponIdStm : findAllStm;
|
||||
|
||||
return stm.all({ weaponSplId }) as Array<AverageAbilityPointsResult>;
|
||||
return stm.all({
|
||||
weaponSplId: weaponSplId ?? null,
|
||||
}) as Array<AverageAbilityPointsResult>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import type {
|
||||
Ability,
|
||||
@@ -9,8 +8,6 @@ import type {
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { filterBuilds } from "./filter.server";
|
||||
|
||||
const FilterBuilds = suite("Filter builds");
|
||||
|
||||
const createBuild = ({
|
||||
headAbilities,
|
||||
modes,
|
||||
@@ -36,246 +33,249 @@ const createBuild = ({
|
||||
};
|
||||
};
|
||||
|
||||
FilterBuilds("returns correct build back based on abilities (AT_LEAST)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 10,
|
||||
comparison: "AT_LEAST",
|
||||
},
|
||||
],
|
||||
describe("Filter builds", () => {
|
||||
test("returns correct build back based on abilities (AT_LEAST)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 10,
|
||||
comparison: "AT_LEAST",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["ISM", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["ISM", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
test("returns correct build back based on abilities (AT_MOST)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 6,
|
||||
comparison: "AT_MOST",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("returns correct build back based on abilities (AT_MOST)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 6,
|
||||
comparison: "AT_MOST",
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["ISS", "ISS", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["ISS", "ISS", "ISM", "ISM"]);
|
||||
});
|
||||
test("filters based on main ability (true)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["T", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "T",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("filters based on main ability (true)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["T", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "T",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["T", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["T", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
test("filters based on main ability (false)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["T", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "T",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("filters based on main ability (false)", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["T", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "T",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["ISS", "ISS", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["ISS", "ISS", "ISM", "ISM"]);
|
||||
});
|
||||
test("filters based on mode", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
modes: ["SZ"],
|
||||
}),
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
modes: null,
|
||||
}),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"], modes: [] }),
|
||||
],
|
||||
count: 3,
|
||||
filters: [
|
||||
{
|
||||
type: "mode",
|
||||
mode: "SZ",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("filters based on mode", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
modes: ["SZ"],
|
||||
}),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"], modes: null }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"], modes: [] }),
|
||||
],
|
||||
count: 3,
|
||||
filters: [
|
||||
{
|
||||
type: "mode",
|
||||
mode: "SZ",
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
test("filters based on many modes", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
modes: ["SZ", "TC"],
|
||||
}),
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
modes: ["SZ"],
|
||||
}),
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
modes: ["TC"],
|
||||
}),
|
||||
],
|
||||
count: 3,
|
||||
filters: [
|
||||
{
|
||||
type: "mode",
|
||||
mode: "SZ",
|
||||
},
|
||||
{
|
||||
type: "mode",
|
||||
mode: "TC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("filters based on many modes", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
modes: ["SZ", "TC"],
|
||||
}),
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
modes: ["SZ"],
|
||||
}),
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
modes: ["TC"],
|
||||
}),
|
||||
],
|
||||
count: 3,
|
||||
filters: [
|
||||
{
|
||||
type: "mode",
|
||||
mode: "SZ",
|
||||
},
|
||||
{
|
||||
type: "mode",
|
||||
mode: "TC",
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
test("filters based on date", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2023, 0, 1)),
|
||||
}),
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2021, 0, 1)),
|
||||
}),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "date",
|
||||
date: "2022-01-01",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("filters based on date", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2023, 0, 1)),
|
||||
}),
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2021, 0, 1)),
|
||||
}),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "date",
|
||||
date: "2022-01-01",
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
test("combines filters of same type", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["T", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["T", "RES", "RES", "RES"] }),
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "T",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 9,
|
||||
comparison: "AT_LEAST",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("combines filters of same type", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["T", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["T", "RES", "RES", "RES"] }),
|
||||
createBuild({ headAbilities: ["ISS", "ISS", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "ability",
|
||||
ability: "T",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 9,
|
||||
comparison: "AT_LEAST",
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["T", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["T", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
test("combines filters of different type", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
// has both
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2023, 0, 1)),
|
||||
}),
|
||||
// has abilities
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2021, 0, 1)),
|
||||
}),
|
||||
// has date
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISS", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2023, 0, 1)),
|
||||
}),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "date",
|
||||
date: "2022-01-01",
|
||||
},
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 9,
|
||||
comparison: "AT_LEAST",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
FilterBuilds("combines filters of different type", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
// has both
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2023, 0, 1)),
|
||||
}),
|
||||
// has abilities
|
||||
createBuild({
|
||||
headAbilities: ["ISM", "ISM", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2021, 0, 1)),
|
||||
}),
|
||||
// has date
|
||||
createBuild({
|
||||
headAbilities: ["ISS", "ISS", "ISM", "ISM"],
|
||||
updatedAt: dateToDatabaseTimestamp(new Date(2023, 0, 1)),
|
||||
}),
|
||||
],
|
||||
count: 2,
|
||||
filters: [
|
||||
{
|
||||
type: "date",
|
||||
date: "2022-01-01",
|
||||
},
|
||||
{
|
||||
type: "ability",
|
||||
ability: "ISM",
|
||||
value: 9,
|
||||
comparison: "AT_LEAST",
|
||||
},
|
||||
],
|
||||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].abilities[0]).toEqual(["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].abilities[0], ["ISS", "ISM", "ISM", "ISM"]);
|
||||
});
|
||||
test("count limits returned builds", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [],
|
||||
});
|
||||
|
||||
FilterBuilds("count limits returned builds", () => {
|
||||
const filtered = filterBuilds({
|
||||
builds: [
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
createBuild({ headAbilities: ["ISM", "ISM", "ISM", "ISM"] }),
|
||||
],
|
||||
count: 2,
|
||||
filters: [],
|
||||
expect(filtered.length).toBe(2);
|
||||
});
|
||||
|
||||
assert.equal(filtered.length, 2);
|
||||
});
|
||||
|
||||
FilterBuilds.run();
|
||||
|
||||
@@ -51,11 +51,14 @@ export const addNewImage = sql.transaction(
|
||||
}) as UserSubmittedImage;
|
||||
|
||||
if (type === "team-pfp") {
|
||||
updateTeamAvatarStm.run({ avatarImgId: img.id, teamId });
|
||||
updateTeamAvatarStm.run({ avatarImgId: img.id, teamId: teamId ?? null });
|
||||
} else if (type === "team-banner") {
|
||||
updateTeamBannerStm.run({ bannerImgId: img.id, teamId });
|
||||
updateTeamBannerStm.run({ bannerImgId: img.id, teamId: teamId ?? null });
|
||||
} else if (type === "org-pfp") {
|
||||
updateOrganizationAvatarStm.run({ avatarImgId: img.id, organizationId });
|
||||
updateOrganizationAvatarStm.run({
|
||||
avatarImgId: img.id,
|
||||
organizationId: organizationId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
return img;
|
||||
|
||||
@@ -461,7 +461,7 @@ function TeamTable({
|
||||
entries: NonNullable<SerializeFrom<typeof loader>["teamLeaderboard"]>;
|
||||
showQualificationDividers?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation("common");
|
||||
const { t } = useTranslation(["common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const isCurrentSeason = data.season === currentSeason(new Date())?.nth;
|
||||
const showQualificationDividers =
|
||||
|
||||
@@ -17,7 +17,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
});
|
||||
|
||||
const identifier = String(user.id);
|
||||
const { team } = (await UserRepository.findByIdentifier(identifier)) ?? {};
|
||||
const { team } =
|
||||
(await UserRepository.findProfileByIdentifier(identifier)) ?? {};
|
||||
|
||||
const shouldIncludeTeam = TEAM_POST_TYPES.includes(data.type);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export function LFGAddFilterButton({
|
||||
<Menu
|
||||
items={Object.entries(defaultFilters).map(([tag, defaultFilter]) => ({
|
||||
id: tag,
|
||||
text: t(`lfg:filters.${tag}`),
|
||||
text: t(`lfg:filters.${tag as LFGFilter["_tag"]}`),
|
||||
disabled: filters.some((filter) => filter._tag === tag),
|
||||
onClick: () => addFilter(defaultFilter),
|
||||
}))}
|
||||
|
||||
@@ -450,7 +450,7 @@ function PostDeleteButton({ id, type }: { id: number; type: Post["type"] }) {
|
||||
|
||||
return (
|
||||
<FormWithConfirm
|
||||
dialogHeading={`Delete post (${t(`lfg:types.${type}`).toLowerCase()})?`}
|
||||
dialogHeading={`Delete post (${(t(`lfg:types.${type}`) as any).toLowerCase()})?`}
|
||||
fields={[
|
||||
["id", id],
|
||||
["_action", "DELETE_POST"],
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as LFGRepository from "../LFGRepository.server";
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
const userProfileData = await UserRepository.findByIdentifier(
|
||||
const userProfileData = await UserRepository.findProfileByIdentifier(
|
||||
String(user.id),
|
||||
);
|
||||
const userQSettingsData = await QSettingsRepository.settingsByUserId(user.id);
|
||||
|
||||
@@ -66,7 +66,7 @@ function encodeURLQuery(filters: LFGFilter[]): string {
|
||||
}
|
||||
|
||||
export default function LFGPage() {
|
||||
const { t } = useTranslation(["common, lfg"]);
|
||||
const { t } = useTranslation(["common", "lfg"]);
|
||||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [filterFromSearch, setTilterFromSearch] = useSearchParamStateEncoder({
|
||||
|
||||
@@ -1,91 +1,92 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
mapPoolToSerializedString,
|
||||
serializedStringToMapPool,
|
||||
} from "./serializer";
|
||||
import type { MapPoolObject } from "./types";
|
||||
|
||||
const Serializer = suite("Map pool serializer");
|
||||
|
||||
const testSerializedPool =
|
||||
"tw:1998000;sz:1d0a000;tc:164c000;rm:15e0000;cb:1ce0000";
|
||||
|
||||
Serializer("Unserializes and then serializes to same result", () => {
|
||||
const mapPool = serializedStringToMapPool(testSerializedPool);
|
||||
describe("Map pool serializer", () => {
|
||||
test("Unserializes and then serializes to same result", () => {
|
||||
const mapPool = serializedStringToMapPool(testSerializedPool);
|
||||
|
||||
assert.equal(mapPoolToSerializedString(mapPool), testSerializedPool);
|
||||
expect(mapPoolToSerializedString(mapPool)).toEqual(testSerializedPool);
|
||||
});
|
||||
|
||||
test("Ignores invalid mode key", () => {
|
||||
const testSerializedPoolWithInvalidMode = `${testSerializedPool};ab:1ce0`;
|
||||
const mapPool = serializedStringToMapPool(
|
||||
testSerializedPoolWithInvalidMode,
|
||||
);
|
||||
|
||||
expect(mapPoolToSerializedString(mapPool)).toEqual(testSerializedPool);
|
||||
});
|
||||
|
||||
test("Matching serialization with IPLMapGen2", () => {
|
||||
const testMapPool: MapPoolObject = {
|
||||
// Gorge, Spillway, Mincemeat, Mahi-Mahi, Inkblot
|
||||
TW: [0, 3, 4, 7, 8],
|
||||
// Gorge, Eeltail, Spillway, Inkblot, MakoMart
|
||||
SZ: [0, 1, 3, 8, 10],
|
||||
// Eeltail, Hagglefish, Bridge, Inbklot, Sturgeon
|
||||
TC: [1, 2, 5, 8, 9],
|
||||
// Eeltail, Spillway, Mincemeat, Bridge, Museum
|
||||
RM: [1, 3, 4, 5, 6],
|
||||
// Gorge, Eeltail, Mincemeat, Bridge, Museum
|
||||
CB: [0, 1, 4, 5, 6],
|
||||
};
|
||||
|
||||
expect(mapPoolToSerializedString(testMapPool)).toEqual(testSerializedPool);
|
||||
});
|
||||
|
||||
test("Omits key if mode has no maps", () => {
|
||||
const testPoolWithoutTw: MapPoolObject = {
|
||||
CB: [1, 2],
|
||||
RM: [1, 8],
|
||||
TC: [8, 4],
|
||||
SZ: [10],
|
||||
TW: [],
|
||||
};
|
||||
|
||||
const serialized = mapPoolToSerializedString(testPoolWithoutTw);
|
||||
|
||||
expect(
|
||||
serialized.includes("sz") && !serialized.includes("tw"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Returns empty string if no maps", () => {
|
||||
const testPoolWithoutTw: MapPoolObject = {
|
||||
CB: [],
|
||||
RM: [],
|
||||
TC: [],
|
||||
SZ: [],
|
||||
TW: [],
|
||||
};
|
||||
|
||||
const serialized = mapPoolToSerializedString(testPoolWithoutTw);
|
||||
|
||||
expect(serialized).toEqual("");
|
||||
});
|
||||
|
||||
test("Value of two modes is the same with same maps", () => {
|
||||
const testPoolWithDuplicateMaps: MapPoolObject = {
|
||||
CB: [1, 2],
|
||||
RM: [1, 2],
|
||||
TC: [],
|
||||
SZ: [],
|
||||
TW: [],
|
||||
};
|
||||
|
||||
const serialized = mapPoolToSerializedString(testPoolWithDuplicateMaps);
|
||||
|
||||
const [modeOne, modeTwo] = serialized.split(";");
|
||||
if (!modeOne || !modeTwo) {
|
||||
throw new Error("Map pool is missing modes");
|
||||
}
|
||||
|
||||
expect(modeOne.split(":")[1]).toEqual(modeTwo.split(":")[1]);
|
||||
});
|
||||
});
|
||||
|
||||
Serializer("Ignores invalid mode key", () => {
|
||||
const testSerializedPoolWithInvalidMode = `${testSerializedPool};ab:1ce0`;
|
||||
const mapPool = serializedStringToMapPool(testSerializedPoolWithInvalidMode);
|
||||
|
||||
assert.equal(mapPoolToSerializedString(mapPool), testSerializedPool);
|
||||
});
|
||||
|
||||
Serializer("Matching serialization with IPLMapGen2", () => {
|
||||
const testMapPool: MapPoolObject = {
|
||||
// Gorge, Spillway, Mincemeat, Mahi-Mahi, Inkblot
|
||||
TW: [0, 3, 4, 7, 8],
|
||||
// Gorge, Eeltail, Spillway, Inkblot, MakoMart
|
||||
SZ: [0, 1, 3, 8, 10],
|
||||
// Eeltail, Hagglefish, Bridge, Inbklot, Sturgeon
|
||||
TC: [1, 2, 5, 8, 9],
|
||||
// Eeltail, Spillway, Mincemeat, Bridge, Museum
|
||||
RM: [1, 3, 4, 5, 6],
|
||||
// Gorge, Eeltail, Mincemeat, Bridge, Museum
|
||||
CB: [0, 1, 4, 5, 6],
|
||||
};
|
||||
|
||||
assert.equal(mapPoolToSerializedString(testMapPool), testSerializedPool);
|
||||
});
|
||||
|
||||
Serializer("Omits key if mode has no maps", () => {
|
||||
const testPoolWithoutTw: MapPoolObject = {
|
||||
CB: [1, 2],
|
||||
RM: [1, 8],
|
||||
TC: [8, 4],
|
||||
SZ: [10],
|
||||
TW: [],
|
||||
};
|
||||
|
||||
const serialized = mapPoolToSerializedString(testPoolWithoutTw);
|
||||
|
||||
assert.ok(serialized.includes("sz") && !serialized.includes("tw"));
|
||||
});
|
||||
|
||||
Serializer("Returns empty string if no maps", () => {
|
||||
const testPoolWithoutTw: MapPoolObject = {
|
||||
CB: [],
|
||||
RM: [],
|
||||
TC: [],
|
||||
SZ: [],
|
||||
TW: [],
|
||||
};
|
||||
|
||||
const serialized = mapPoolToSerializedString(testPoolWithoutTw);
|
||||
|
||||
assert.equal(serialized, "");
|
||||
});
|
||||
|
||||
Serializer("Value of two modes is the same with same maps", () => {
|
||||
const testPoolWithDuplicateMaps: MapPoolObject = {
|
||||
CB: [1, 2],
|
||||
RM: [1, 2],
|
||||
TC: [],
|
||||
SZ: [],
|
||||
TW: [],
|
||||
};
|
||||
|
||||
const serialized = mapPoolToSerializedString(testPoolWithDuplicateMaps);
|
||||
|
||||
const [modeOne, modeTwo] = serialized.split(";");
|
||||
if (!modeOne || !modeTwo) {
|
||||
throw new Error("Map pool is missing modes");
|
||||
}
|
||||
|
||||
assert.equal(modeOne.split(":")[1], modeTwo.split(":")[1]);
|
||||
});
|
||||
|
||||
Serializer.run();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Rating, Team } from "node_modules/openskill/dist/types";
|
||||
import { rate as openskillRate, ordinal, rating } from "openskill";
|
||||
import type { Rating, Team } from "openskill/dist/types";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { TierName } from "./mmr-constants";
|
||||
import { TIERS } from "./mmr-constants";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { buildStats } from "~/features/build-analyzer";
|
||||
import type {
|
||||
AbilityPoints,
|
||||
@@ -54,128 +53,126 @@ function calculate({
|
||||
});
|
||||
}
|
||||
|
||||
const CalculateDamage = suite("calculateDamage()");
|
||||
describe("calculateDamage()", () => {
|
||||
// the function throws if weapon resolves to more than one set of damage rates
|
||||
// so this test goes through all of them to make sure they all work
|
||||
test("Every weapon can calculate damage", () => {
|
||||
for (const mainWeaponId of mainWeaponIds) {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: mainWeaponId,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
// the function throws if weapon resolves to more than one set of damage rates
|
||||
// so this test goes through all of them to make sure they all work
|
||||
CalculateDamage("Every weapon can calculate damage", () => {
|
||||
for (const mainWeaponId of mainWeaponIds) {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: mainWeaponId,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
for (const damage of analyzed.stats.damages) {
|
||||
calculate({ mainWeaponId, damageType: damage.type });
|
||||
for (const damage of analyzed.stats.damages) {
|
||||
calculate({ mainWeaponId, damageType: damage.type });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: 0,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
for (const damage of analyzed.stats.subWeaponDefenseDamages) {
|
||||
calculate({
|
||||
subWeaponId: damage.subWeaponId,
|
||||
damageType: damage.type,
|
||||
preAnalyzed: analyzed,
|
||||
});
|
||||
}
|
||||
|
||||
for (const specialWeaponId of specialWeaponIds) {
|
||||
const analyzedWithSpecialWeapon = buildStats({
|
||||
weaponSplId: exampleMainWeaponIdWithSpecialWeaponId(specialWeaponId),
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: 0,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
for (const damage of analyzedWithSpecialWeapon.stats.specialWeaponDamages) {
|
||||
for (const damage of analyzed.stats.subWeaponDefenseDamages) {
|
||||
calculate({
|
||||
specialWeaponId,
|
||||
subWeaponId: damage.subWeaponId,
|
||||
damageType: damage.type,
|
||||
preAnalyzed: analyzedWithSpecialWeapon,
|
||||
preAnalyzed: analyzed,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CalculateDamage("BRU increases Splash Wall hitpoints", () => {
|
||||
const withoutBRU = calculate({});
|
||||
const withBRU = calculate({
|
||||
abilityPoints: new Map([["BRU", 10]]),
|
||||
for (const specialWeaponId of specialWeaponIds) {
|
||||
const analyzedWithSpecialWeapon = buildStats({
|
||||
weaponSplId: exampleMainWeaponIdWithSpecialWeaponId(specialWeaponId),
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
for (const damage of analyzedWithSpecialWeapon.stats
|
||||
.specialWeaponDamages) {
|
||||
calculate({
|
||||
specialWeaponId,
|
||||
damageType: damage.type,
|
||||
preAnalyzed: analyzedWithSpecialWeapon,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const hpWithoutBRU = withoutBRU.find(
|
||||
(d) => d.receiver === "Wsb_Shield",
|
||||
)?.hitPoints;
|
||||
const hpWithBRU = withBRU.find((d) => d.receiver === "Wsb_Shield")?.hitPoints;
|
||||
test("BRU increases Splash Wall hitpoints", () => {
|
||||
const withoutBRU = calculate({});
|
||||
const withBRU = calculate({
|
||||
abilityPoints: new Map([["BRU", 10]]),
|
||||
});
|
||||
|
||||
assert.ok(typeof hpWithoutBRU === "number");
|
||||
assert.ok(typeof hpWithBRU === "number");
|
||||
assert.ok(hpWithoutBRU < hpWithBRU);
|
||||
});
|
||||
const hpWithoutBRU = withoutBRU.find(
|
||||
(d) => d.receiver === "Wsb_Shield",
|
||||
)?.hitPoints;
|
||||
const hpWithBRU = withBRU.find((d) => d.receiver === "Wsb_Shield")
|
||||
?.hitPoints!;
|
||||
|
||||
CalculateDamage("SPU increases Big Bubbler hitpoints", () => {
|
||||
const withoutSPU = calculate({});
|
||||
const withSPU = calculate({
|
||||
abilityPoints: new Map([["SPU", 10]]),
|
||||
expect(typeof hpWithoutBRU).toBe("number");
|
||||
expect(typeof hpWithBRU).toBe("number");
|
||||
expect(hpWithoutBRU).toBeLessThan(hpWithBRU);
|
||||
});
|
||||
|
||||
const hpWithoutSPU = withoutSPU.find(
|
||||
(d) => d.receiver === "GreatBarrier_Barrier",
|
||||
)?.hitPoints;
|
||||
const hpWithSPU = withSPU.find(
|
||||
(d) => d.receiver === "GreatBarrier_Barrier",
|
||||
)?.hitPoints;
|
||||
test("SPU increases Big Bubbler hitpoints", () => {
|
||||
const withoutSPU = calculate({});
|
||||
const withSPU = calculate({
|
||||
abilityPoints: new Map([["SPU", 10]]),
|
||||
});
|
||||
|
||||
assert.ok(typeof hpWithoutSPU === "number");
|
||||
assert.ok(typeof hpWithSPU === "number");
|
||||
assert.ok(hpWithoutSPU < hpWithSPU);
|
||||
});
|
||||
const hpWithoutSPU = withoutSPU.find(
|
||||
(d) => d.receiver === "GreatBarrier_Barrier",
|
||||
)?.hitPoints;
|
||||
const hpWithSPU = withSPU.find((d) => d.receiver === "GreatBarrier_Barrier")
|
||||
?.hitPoints!;
|
||||
|
||||
const shotsToPopRM: Array<
|
||||
[
|
||||
weaponId: MainWeaponId,
|
||||
damageType: DamageType,
|
||||
shotsToPop: number,
|
||||
shotsToPopOS: number,
|
||||
]
|
||||
> = [
|
||||
// Splattershot
|
||||
[40, "NORMAL_MAX", 28, 26],
|
||||
// Range Blaster
|
||||
[220, "DIRECT", 5, 4],
|
||||
// .96 Gal
|
||||
[80, "NORMAL_MAX", 17, 15],
|
||||
// Luna Blaster
|
||||
[200, "DIRECT", 4, 4],
|
||||
// Splat Charger
|
||||
[2010, "FULL_CHARGE", 4, 3],
|
||||
// E-liter 4K
|
||||
[2030, "TAP_SHOT", 13, 12],
|
||||
// Hydra Splatling
|
||||
[4020, "NORMAL_MAX", 32, 29],
|
||||
// Sloshing Machine
|
||||
[3020, "DIRECT_MAX", 6, 5],
|
||||
// Splat Dualies
|
||||
[5010, "NORMAL_MAX", 34, 31],
|
||||
// Tenta Brella
|
||||
[6010, "NORMAL_MAX", 4, 4],
|
||||
// // Tri-Stringer
|
||||
[7010, "NORMAL_MAX", 3, 3],
|
||||
// REEF-LUX
|
||||
[7020, "NORMAL_MIN", 8, 7],
|
||||
// Splatana Wiper
|
||||
[8010, "SPLATANA_HORIZONTAL", 11, 10],
|
||||
// Splatana Wiper
|
||||
[8010, "SPLATANA_HORIZONTAL_DIRECT", 9, 8],
|
||||
// Splatana Stamper
|
||||
[8000, "SPLATANA_VERTICAL_DIRECT", 3, 3],
|
||||
];
|
||||
expect(typeof hpWithoutSPU).toBe("number");
|
||||
expect(typeof hpWithSPU).toBe("number");
|
||||
expect(hpWithoutSPU).toBeLessThan(hpWithSPU);
|
||||
});
|
||||
|
||||
CalculateDamage(
|
||||
"Calculates matching HTD Rainmaker shield to in-game tests",
|
||||
() => {
|
||||
const shotsToPopRM: Array<
|
||||
[
|
||||
weaponId: MainWeaponId,
|
||||
damageType: DamageType,
|
||||
shotsToPop: number,
|
||||
shotsToPopOS: number,
|
||||
]
|
||||
> = [
|
||||
// Splattershot
|
||||
[40, "NORMAL_MAX", 28, 26],
|
||||
// Range Blaster
|
||||
[220, "DIRECT", 5, 4],
|
||||
// .96 Gal
|
||||
[80, "NORMAL_MAX", 17, 15],
|
||||
// Luna Blaster
|
||||
[200, "DIRECT", 4, 4],
|
||||
// Splat Charger
|
||||
[2010, "FULL_CHARGE", 4, 3],
|
||||
// E-liter 4K
|
||||
[2030, "TAP_SHOT", 13, 12],
|
||||
// Hydra Splatling
|
||||
[4020, "NORMAL_MAX", 32, 29],
|
||||
// Sloshing Machine
|
||||
[3020, "DIRECT_MAX", 6, 5],
|
||||
// Splat Dualies
|
||||
[5010, "NORMAL_MAX", 34, 31],
|
||||
// Tenta Brella
|
||||
[6010, "NORMAL_MAX", 4, 4],
|
||||
// // Tri-Stringer
|
||||
[7010, "NORMAL_MAX", 3, 3],
|
||||
// REEF-LUX
|
||||
[7020, "NORMAL_MIN", 8, 7],
|
||||
// Splatana Wiper
|
||||
[8010, "SPLATANA_HORIZONTAL", 11, 10],
|
||||
// Splatana Wiper
|
||||
[8010, "SPLATANA_HORIZONTAL_DIRECT", 9, 8],
|
||||
// Splatana Stamper
|
||||
[8000, "SPLATANA_VERTICAL_DIRECT", 3, 3],
|
||||
];
|
||||
|
||||
test("Calculates matching HTD Rainmaker shield to in-game tests", () => {
|
||||
for (const [
|
||||
mainWeaponId,
|
||||
damageType,
|
||||
@@ -188,24 +185,19 @@ CalculateDamage(
|
||||
(d) => d.receiver === "Gachihoko_Barrier",
|
||||
)!;
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
damageVsRM.damages.find((d) => !d.objectShredder)!.hitsToDestroy,
|
||||
shotsToPop,
|
||||
`Shots to pop wrong for weapon id: ${mainWeaponId}`,
|
||||
);
|
||||
assert.equal(
|
||||
).toBe(shotsToPop);
|
||||
expect(
|
||||
damageVsRM.damages.find((d) => d.objectShredder)!.hitsToDestroy,
|
||||
shotsToPopOS,
|
||||
`Shots to pop wrong with OS for weapon id: ${mainWeaponId}`,
|
||||
);
|
||||
).toBe(shotsToPopOS);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const HYDRA_SPLATLING_ID = 4020;
|
||||
CalculateDamage(
|
||||
"Hits to destroy Minimum < Maximum < Maximum (Fully charged)",
|
||||
() => {
|
||||
const HYDRA_SPLATLING_ID = 4020;
|
||||
test("Hits to destroy Minimum < Maximum < Maximum (Fully charged)", () => {
|
||||
const min = calculate({
|
||||
mainWeaponId: HYDRA_SPLATLING_ID,
|
||||
damageType: "NORMAL_MIN",
|
||||
@@ -219,13 +211,11 @@ CalculateDamage(
|
||||
damageType: "NORMAL_MAX_FULL_CHARGE",
|
||||
})[0]?.damages[0]?.hitsToDestroy;
|
||||
|
||||
assert.ok(typeof min === "number");
|
||||
assert.ok(typeof max === "number");
|
||||
assert.ok(typeof maxFullyCharged === "number");
|
||||
expect(typeof min).toBe("number");
|
||||
expect(typeof max).toBe("number");
|
||||
expect(typeof maxFullyCharged).toBe("number");
|
||||
|
||||
assert.ok(min > max);
|
||||
assert.ok(max > maxFullyCharged);
|
||||
},
|
||||
);
|
||||
|
||||
CalculateDamage.run();
|
||||
expect(min).toBeGreaterThan(max);
|
||||
expect(max).toBeGreaterThan(maxFullyCharged);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { tierDifferenceToRangeOrExact } from "./groups.server";
|
||||
|
||||
const paramsToExpected = new Map<
|
||||
@@ -64,29 +63,24 @@ const paramsToExpected = new Map<
|
||||
{ isPlus: false, name: "DIAMOND" },
|
||||
);
|
||||
|
||||
const TierDifferenceToRangeOrExact = suite("tierDifferenceToRangeOrExact()");
|
||||
|
||||
for (const [input, expected] of paramsToExpected) {
|
||||
TierDifferenceToRangeOrExact(
|
||||
`works for ${JSON.stringify(input)} -> ${JSON.stringify(expected)}`,
|
||||
() => {
|
||||
describe("tierDifferenceToRangeOrExact()", () => {
|
||||
for (const [input, expected] of paramsToExpected) {
|
||||
test(`works for ${JSON.stringify(input)} -> ${JSON.stringify(expected)}`, () => {
|
||||
const result = tierDifferenceToRangeOrExact({
|
||||
ourTier: input[0],
|
||||
theirTier: input[1],
|
||||
hasLeviathan: true,
|
||||
}).tier;
|
||||
assert.equal(result, expected);
|
||||
},
|
||||
);
|
||||
}
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
}
|
||||
|
||||
TierDifferenceToRangeOrExact("works before leviathan", () => {
|
||||
const result = tierDifferenceToRangeOrExact({
|
||||
ourTier: { isPlus: true, name: "DIAMOND" },
|
||||
theirTier: { isPlus: false, name: "DIAMOND" },
|
||||
hasLeviathan: false,
|
||||
}).tier;
|
||||
assert.equal(result, { isPlus: false, name: "DIAMOND" });
|
||||
test("works before leviathan", () => {
|
||||
const result = tierDifferenceToRangeOrExact({
|
||||
ourTier: { isPlus: true, name: "DIAMOND" },
|
||||
theirTier: { isPlus: false, name: "DIAMOND" },
|
||||
hasLeviathan: false,
|
||||
}).tier;
|
||||
expect(result).toEqual({ isPlus: false, name: "DIAMOND" });
|
||||
});
|
||||
});
|
||||
|
||||
TierDifferenceToRangeOrExact.run();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { UserMapModePreferences } from "~/db/tables";
|
||||
import type { StageId } from "~/modules/in-game-lists";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
@@ -8,18 +7,16 @@ import * as Test from "~/utils/Test";
|
||||
import { nullFilledArray } from "~/utils/arrays";
|
||||
import { mapLottery, mapModePreferencesToModeList } from "./match.server";
|
||||
|
||||
const MapModePreferencesToModeList = suite("mapModePreferencesToModeList()");
|
||||
const MapPoolFromPreferences = suite("mapPoolFromPreferences()");
|
||||
describe("mapModePreferencesToModeList()", () => {
|
||||
test("returns default list if no preferences", () => {
|
||||
const modeList = mapModePreferencesToModeList([], []);
|
||||
|
||||
MapModePreferencesToModeList("returns default list if no preferences", () => {
|
||||
const modeList = mapModePreferencesToModeList([], []);
|
||||
expect(
|
||||
Test.arrayContainsSameItems(["SZ", "TC", "RM", "CB"], modeList),
|
||||
).toBeTrue();
|
||||
});
|
||||
|
||||
assert.ok(Test.arrayContainsSameItems(["SZ", "TC", "RM", "CB"], modeList));
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList(
|
||||
"returns default list if equally disliking everything",
|
||||
() => {
|
||||
test("returns default list if equally disliking everything", () => {
|
||||
const dislikingEverything = [
|
||||
{ mode: "TW", preference: "AVOID" } as const,
|
||||
{ mode: "SZ", preference: "AVOID" } as const,
|
||||
@@ -43,25 +40,23 @@ MapModePreferencesToModeList(
|
||||
],
|
||||
);
|
||||
|
||||
assert.ok(Test.arrayContainsSameItems(["SZ", "TC", "RM", "CB"], modeList));
|
||||
},
|
||||
);
|
||||
expect(
|
||||
Test.arrayContainsSameItems(["SZ", "TC", "RM", "CB"], modeList),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList(
|
||||
"if positive about nothing, choose the most liked (-TW)",
|
||||
() => {
|
||||
test("if positive about nothing, choose the most liked (-TW)", () => {
|
||||
const modeList = mapModePreferencesToModeList(
|
||||
[[{ mode: "SZ", preference: "AVOID" }]],
|
||||
[],
|
||||
);
|
||||
|
||||
assert.ok(Test.arrayContainsSameItems(["TC", "RM", "CB"], modeList));
|
||||
},
|
||||
);
|
||||
expect(Test.arrayContainsSameItems(["TC", "RM", "CB"], modeList)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList(
|
||||
"only turf war possible to get if least bad option",
|
||||
() => {
|
||||
test("only turf war possible to get if least bad option", () => {
|
||||
const modeList = mapModePreferencesToModeList(
|
||||
[
|
||||
[
|
||||
@@ -76,110 +71,98 @@ MapModePreferencesToModeList(
|
||||
[],
|
||||
);
|
||||
|
||||
assert.ok(Test.arrayContainsSameItems(["TW"], modeList));
|
||||
},
|
||||
);
|
||||
expect(Test.arrayContainsSameItems(["TW"], modeList)).toBe(true);
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList("team votes for their preference", () => {
|
||||
const modeList = mapModePreferencesToModeList(
|
||||
[
|
||||
test("team votes for their preference", () => {
|
||||
const modeList = mapModePreferencesToModeList(
|
||||
[
|
||||
{ mode: "SZ", preference: "PREFER" },
|
||||
{ mode: "TC", preference: "PREFER" },
|
||||
[
|
||||
{ mode: "SZ", preference: "PREFER" },
|
||||
{ mode: "TC", preference: "PREFER" },
|
||||
],
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
[{ mode: "TC", preference: "AVOID" }],
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
],
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
[{ mode: "TC", preference: "AVOID" }],
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
],
|
||||
[
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
[{ mode: "TC", preference: "AVOID" }],
|
||||
[{ mode: "TC", preference: "AVOID" }],
|
||||
],
|
||||
);
|
||||
[
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
[{ mode: "TC", preference: "PREFER" }],
|
||||
[{ mode: "TC", preference: "AVOID" }],
|
||||
[{ mode: "TC", preference: "AVOID" }],
|
||||
],
|
||||
);
|
||||
|
||||
assert.ok(Test.arrayContainsSameItems(["SZ", "TC"], modeList));
|
||||
});
|
||||
expect(Test.arrayContainsSameItems(["SZ", "TC"], modeList)).toBe(true);
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList(
|
||||
"favorite ranked mode sorted first in the array",
|
||||
() => {
|
||||
assert.equal(
|
||||
test("favorite ranked mode sorted first in the array", () => {
|
||||
expect(
|
||||
mapModePreferencesToModeList(
|
||||
[[{ mode: "TC", preference: "PREFER" }]],
|
||||
[],
|
||||
)[0],
|
||||
"TC",
|
||||
);
|
||||
},
|
||||
);
|
||||
).toBe("TC");
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList(
|
||||
"includes turf war if more prefer than want to avoid",
|
||||
() => {
|
||||
test("includes turf war if more prefer than want to avoid", () => {
|
||||
const modeList = mapModePreferencesToModeList(
|
||||
[[{ mode: "TW", preference: "PREFER" }]],
|
||||
[[{ mode: "SZ", preference: "PREFER" }]],
|
||||
);
|
||||
|
||||
assert.ok(Test.arrayContainsSameItems(["TW", "SZ"], modeList));
|
||||
},
|
||||
);
|
||||
expect(Test.arrayContainsSameItems(["TW", "SZ"], modeList)).toBe(true);
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList("doesn't include turf war if mixed", () => {
|
||||
const modeList = mapModePreferencesToModeList(
|
||||
[[{ mode: "TW", preference: "PREFER" }]],
|
||||
[[{ mode: "TW", preference: "AVOID" }]],
|
||||
);
|
||||
test("doesn't include turf war if mixed", () => {
|
||||
const modeList = mapModePreferencesToModeList(
|
||||
[[{ mode: "TW", preference: "PREFER" }]],
|
||||
[[{ mode: "TW", preference: "AVOID" }]],
|
||||
);
|
||||
|
||||
assert.ok(Test.arrayContainsSameItems(["SZ", "TC", "RM", "CB"], modeList));
|
||||
expect(
|
||||
Test.arrayContainsSameItems(["SZ", "TC", "RM", "CB"], modeList),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
const MODES_COUNT = 4;
|
||||
const STAGES_PER_MODE = 7;
|
||||
|
||||
MapPoolFromPreferences("returns maps even if no preferences", () => {
|
||||
const mapPool = mapLottery([], rankedModesShort);
|
||||
describe("mapLottery()", () => {
|
||||
test("returns maps even if no preferences", () => {
|
||||
const mapPool = mapLottery([], rankedModesShort);
|
||||
|
||||
assert.equal(mapPool.stageModePairs.length, STAGES_PER_MODE * MODES_COUNT);
|
||||
});
|
||||
expect(mapPool.stageModePairs.length).toBe(STAGES_PER_MODE * MODES_COUNT);
|
||||
});
|
||||
|
||||
MapPoolFromPreferences("returns some maps from the map pools", () => {
|
||||
const memberOnePool: UserMapModePreferences["pool"] = rankedModesShort.map(
|
||||
(mode) => ({
|
||||
mode,
|
||||
stages: nullFilledArray(7).map((_, i) => (i + 1) as StageId),
|
||||
}),
|
||||
);
|
||||
const memberTwoPool: UserMapModePreferences["pool"] = rankedModesShort.map(
|
||||
(mode) => ({
|
||||
mode,
|
||||
stages: nullFilledArray(7).map((_, i) => (i + 10) as StageId),
|
||||
}),
|
||||
);
|
||||
test("returns some maps from the map pools", () => {
|
||||
const memberOnePool: UserMapModePreferences["pool"] = rankedModesShort.map(
|
||||
(mode) => ({
|
||||
mode,
|
||||
stages: nullFilledArray(7).map((_, i) => (i + 1) as StageId),
|
||||
}),
|
||||
);
|
||||
const memberTwoPool: UserMapModePreferences["pool"] = rankedModesShort.map(
|
||||
(mode) => ({
|
||||
mode,
|
||||
stages: nullFilledArray(7).map((_, i) => (i + 10) as StageId),
|
||||
}),
|
||||
);
|
||||
|
||||
const pool = mapLottery(
|
||||
[
|
||||
{ modes: [], pool: memberOnePool },
|
||||
{ modes: [], pool: memberTwoPool },
|
||||
],
|
||||
rankedModesShort,
|
||||
);
|
||||
const pool = mapLottery(
|
||||
[
|
||||
{ modes: [], pool: memberOnePool },
|
||||
{ modes: [], pool: memberTwoPool },
|
||||
],
|
||||
rankedModesShort,
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
pool.stageModePairs.some((p) => p.stageId <= 7),
|
||||
"No map from memberOnePool",
|
||||
);
|
||||
assert.ok(
|
||||
pool.stageModePairs.some((p) => p.stageId > 10),
|
||||
"No map from memberTwoPool",
|
||||
);
|
||||
});
|
||||
expect(pool.stageModePairs.some((p) => p.stageId <= 7)).toBe(true);
|
||||
expect(pool.stageModePairs.some((p) => p.stageId > 10)).toBe(true);
|
||||
});
|
||||
|
||||
MapPoolFromPreferences(
|
||||
"includes modes that were given and nothing else",
|
||||
() => {
|
||||
test("includes modes that were given and nothing else", () => {
|
||||
const memberOnePool: UserMapModePreferences["pool"] = rankedModesShort.map(
|
||||
(mode) => ({
|
||||
mode,
|
||||
@@ -189,31 +172,28 @@ MapPoolFromPreferences(
|
||||
|
||||
const pool = mapLottery([{ modes: [], pool: memberOnePool }], ["SZ", "TC"]);
|
||||
|
||||
assert.ok(
|
||||
expect(
|
||||
pool.stageModePairs.every((p) => p.mode === "SZ" || p.mode === "TC"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("excludes map preferences if mode is avoided", () => {
|
||||
const memberOnePool: UserMapModePreferences["pool"] = [
|
||||
{
|
||||
mode: "SZ",
|
||||
stages: nullFilledArray(7).map((_, i) => (i + 1) as StageId),
|
||||
},
|
||||
];
|
||||
|
||||
const pool = mapLottery(
|
||||
[{ modes: [{ preference: "AVOID", mode: "SZ" }], pool: memberOnePool }],
|
||||
["SZ"],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MapPoolFromPreferences("excludes map preferences if mode is avoided", () => {
|
||||
const memberOnePool: UserMapModePreferences["pool"] = [
|
||||
{
|
||||
mode: "SZ",
|
||||
stages: nullFilledArray(7).map((_, i) => (i + 1) as StageId),
|
||||
},
|
||||
];
|
||||
|
||||
const pool = mapLottery(
|
||||
[{ modes: [{ preference: "AVOID", mode: "SZ" }], pool: memberOnePool }],
|
||||
["SZ"],
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
pool.stageModePairs.every((p) =>
|
||||
SENDOUQ_DEFAULT_MAPS.SZ.some((stageId) => stageId === p.stageId),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
pool.stageModePairs.every((p) =>
|
||||
SENDOUQ_DEFAULT_MAPS.SZ.some((stageId) => stageId === p.stageId),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
MapModePreferencesToModeList.run();
|
||||
MapPoolFromPreferences.run();
|
||||
|
||||
@@ -1,110 +1,107 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { mergeReportedWeapons } from "./reported-weapons.server";
|
||||
|
||||
const MergeReportedWeapons = suite("mergeReportedWeapons()");
|
||||
describe("mergeReportedWeapons()", () => {
|
||||
const newWeapons = [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 1,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
];
|
||||
|
||||
const newWeapons: Parameters<typeof mergeReportedWeapons>[0]["newWeapons"] = [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 1,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
];
|
||||
test("handles no old weapons", () => {
|
||||
const result = mergeReportedWeapons({ newWeapons, oldWeapons: [] });
|
||||
|
||||
MergeReportedWeapons("handles no old weapons", () => {
|
||||
const result = mergeReportedWeapons({ newWeapons, oldWeapons: [] });
|
||||
|
||||
assert.equal(result, newWeapons);
|
||||
});
|
||||
|
||||
MergeReportedWeapons("replaces a weapon", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 1,
|
||||
weaponSplId: 1 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
expect(result).toEqual(newWeapons);
|
||||
});
|
||||
|
||||
assert.equal(result, newWeapons);
|
||||
});
|
||||
test("replaces a weapon", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 1,
|
||||
weaponSplId: 1 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
MergeReportedWeapons("merges two completely separate lists", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
expect(result).toEqual(newWeapons);
|
||||
});
|
||||
|
||||
test("merges two completely separate lists", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 2,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 2,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
...newWeapons,
|
||||
]);
|
||||
});
|
||||
|
||||
assert.equal(result, [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 2,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
...newWeapons,
|
||||
]);
|
||||
});
|
||||
test("handles merging partially same list", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 1,
|
||||
weaponSplId: 1 as MainWeaponId,
|
||||
},
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 2,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
MergeReportedWeapons("handles merging partially same list", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 1,
|
||||
weaponSplId: 1 as MainWeaponId,
|
||||
},
|
||||
expect(result).toEqual([
|
||||
...newWeapons,
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 2,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
assert.equal(result, [
|
||||
...newWeapons,
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 0,
|
||||
userId: 2,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
]);
|
||||
});
|
||||
test("slices unplayed maps", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 1,
|
||||
userId: 1,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
newReportedMapsCount: 1,
|
||||
});
|
||||
|
||||
MergeReportedWeapons("slices unplayed maps", () => {
|
||||
const result = mergeReportedWeapons({
|
||||
newWeapons,
|
||||
oldWeapons: [
|
||||
{
|
||||
groupMatchMapId: 1,
|
||||
mapIndex: 1,
|
||||
userId: 1,
|
||||
weaponSplId: 0 as MainWeaponId,
|
||||
},
|
||||
],
|
||||
newReportedMapsCount: 1,
|
||||
expect(result).toEqual(newWeapons);
|
||||
});
|
||||
|
||||
assert.equal(result, newWeapons);
|
||||
});
|
||||
|
||||
MergeReportedWeapons.run();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Rating } from "node_modules/openskill/dist/types";
|
||||
import { ordinal } from "openskill";
|
||||
import type { Rating } from "openskill/dist/types";
|
||||
import type {
|
||||
Group,
|
||||
GroupMatch,
|
||||
|
||||
@@ -31,6 +31,15 @@ const addPlayerResultDeltaStm = sql.prepare(/* sql */ `
|
||||
|
||||
export function addPlayerResults(results: Array<PlayerResult>) {
|
||||
for (const result of results) {
|
||||
addPlayerResultDeltaStm.run(result);
|
||||
addPlayerResultDeltaStm.run({
|
||||
ownerUserId: result.ownerUserId,
|
||||
otherUserId: result.otherUserId,
|
||||
mapWins: result.mapWins,
|
||||
mapLosses: result.mapLosses,
|
||||
setWins: result.setWins,
|
||||
setLosses: result.setLosses,
|
||||
type: result.type,
|
||||
season: result.season,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ const stm = sql.prepare(/* sql */ `
|
||||
`);
|
||||
|
||||
export function chatCodeByGroupId(id: number) {
|
||||
return stm.pluck().get({ id }) as string | undefined;
|
||||
return (stm.get({ id }) as any)?.chatCode as string | undefined;
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@ const stm = sql.prepare(/* sql */ `
|
||||
`);
|
||||
|
||||
export function groupSize(groupId: number) {
|
||||
return stm.pluck().get({ groupId }) as number;
|
||||
return (stm.get({ groupId }) as any).count as number;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import type { SerializeFrom } from "@remix-run/server-runtime";
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { db } from "~/db/sql";
|
||||
import type { UserMapModePreferences } from "~/db/tables";
|
||||
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
|
||||
import { stageIds } from "~/modules/in-game-lists";
|
||||
import * as Test from "~/utils/Test";
|
||||
import {
|
||||
dbInsertUsers,
|
||||
dbReset,
|
||||
wrappedAction,
|
||||
wrappedLoader,
|
||||
} from "~/utils/Test";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { lookingSchema, matchSchema } from "../q-schemas.server";
|
||||
import { loader, action as rawLookingAction } from "./q.looking";
|
||||
import { action as rawMatchAction } from "./q.match.$id";
|
||||
|
||||
const SendouQMatchCreation = suite("SendouQ match creation");
|
||||
const PrivateUserNoteSorting = suite("Private user note sorting");
|
||||
|
||||
const lookingAction = Test.wrappedAction<typeof lookingSchema>({
|
||||
action: rawLookingAction,
|
||||
});
|
||||
|
||||
const createGroup = async (userIds: number[]) => {
|
||||
const group = await db
|
||||
.insertInto("Group")
|
||||
@@ -47,7 +45,7 @@ const SZ_ONLY_PREFERENCE: UserMapModePreferences["modes"] = [
|
||||
];
|
||||
|
||||
const prepareGroups = async () => {
|
||||
await Test.database.insertUsers(8);
|
||||
await dbInsertUsers(8);
|
||||
await createGroup([1, 2, 3, 4]);
|
||||
await createGroup([5, 6, 7, 8]);
|
||||
await db
|
||||
@@ -81,6 +79,10 @@ const insertMapModePreferences = (
|
||||
.execute();
|
||||
};
|
||||
|
||||
const lookingAction = wrappedAction<typeof lookingSchema>({
|
||||
action: rawLookingAction,
|
||||
});
|
||||
|
||||
const createMatch = () =>
|
||||
lookingAction(
|
||||
{
|
||||
@@ -97,65 +99,68 @@ const findMatch = () =>
|
||||
.where("id", "=", 1)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
SendouQMatchCreation.before.each(async () => {
|
||||
await prepareGroups();
|
||||
});
|
||||
|
||||
SendouQMatchCreation.after.each(() => {
|
||||
Test.database.reset();
|
||||
});
|
||||
|
||||
SendouQMatchCreation("adds pools to memento", async () => {
|
||||
await createMatch();
|
||||
|
||||
const match = await findMatch();
|
||||
const pools = match.memento?.pools;
|
||||
|
||||
invariant(pools, "pools missing");
|
||||
|
||||
assert.equal(pools.length, 2);
|
||||
assert.ok(pools.some((p) => p.pool[0].stages.includes(1)));
|
||||
assert.ok(pools.some((p) => p.pool[0].stages.includes(19)));
|
||||
});
|
||||
|
||||
SendouQMatchCreation("doesn't add pool where mode is avoided", async () => {
|
||||
await insertMapModePreferences(1, {
|
||||
modes: [
|
||||
{ mode: "SZ", preference: "AVOID" },
|
||||
{ mode: "TC", preference: "PREFER" },
|
||||
],
|
||||
pool: [
|
||||
{ mode: "SZ", stages: [...stageIds].slice(0, 7) },
|
||||
{ mode: "TC", stages: [...stageIds].slice(0, 7) },
|
||||
],
|
||||
describe("SendouQ match creation", () => {
|
||||
beforeEach(async () => {
|
||||
await prepareGroups();
|
||||
});
|
||||
|
||||
await createMatch();
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
const match = await findMatch();
|
||||
const pools = match.memento?.pools;
|
||||
test("adds pools to memento", async () => {
|
||||
await createMatch();
|
||||
|
||||
invariant(pools, "pools missing");
|
||||
const match = await findMatch();
|
||||
const pools = match.memento?.pools;
|
||||
|
||||
assert.equal(pools.length, 2);
|
||||
assert.ok(
|
||||
pools.find((p) => p.userId === 1)!.pool.every((p) => p.mode !== "SZ"),
|
||||
);
|
||||
});
|
||||
invariant(pools, "pools missing");
|
||||
|
||||
SendouQMatchCreation("adds mode preferences to memento", async () => {
|
||||
await createMatch();
|
||||
expect(pools.length).toBe(2);
|
||||
expect(pools.some((p) => p.pool[0].stages.includes(1))).toBe(true);
|
||||
expect(pools.some((p) => p.pool[0].stages.includes(19))).toBe(true);
|
||||
});
|
||||
|
||||
const match = await findMatch();
|
||||
test("doesn't add pool where mode is avoided", async () => {
|
||||
await insertMapModePreferences(1, {
|
||||
modes: [
|
||||
{ mode: "SZ", preference: "AVOID" },
|
||||
{ mode: "TC", preference: "PREFER" },
|
||||
],
|
||||
pool: [
|
||||
{
|
||||
mode: "TC",
|
||||
stages: [...stageIds]
|
||||
.filter((stageId) => !BANNED_MAPS.TC.includes(stageId))
|
||||
.slice(0, 7),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const modePreferences = match.memento?.modePreferences;
|
||||
await createMatch();
|
||||
|
||||
assert.equal(modePreferences?.SZ?.length, 2);
|
||||
});
|
||||
const match = await findMatch();
|
||||
const pools = match.memento?.pools;
|
||||
|
||||
SendouQMatchCreation(
|
||||
"adds mode preferences to memento including neutral",
|
||||
async () => {
|
||||
invariant(pools, "pools missing");
|
||||
|
||||
expect(pools.length).toBe(2);
|
||||
expect(
|
||||
pools.find((p) => p.userId === 1)!.pool.every((p) => p.mode !== "SZ"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("adds mode preferences to memento", async () => {
|
||||
await createMatch();
|
||||
|
||||
const match = await findMatch();
|
||||
|
||||
const modePreferences = match.memento?.modePreferences;
|
||||
|
||||
expect(modePreferences?.SZ?.length).toBe(2);
|
||||
});
|
||||
|
||||
test("adds mode preferences to memento including neutral", async () => {
|
||||
await insertMapModePreferences(2, {
|
||||
modes: [{ mode: "TC", preference: "PREFER" }],
|
||||
pool: [],
|
||||
@@ -167,78 +172,76 @@ SendouQMatchCreation(
|
||||
|
||||
const modePreferences = match.memento?.modePreferences;
|
||||
|
||||
assert.equal(modePreferences?.SZ?.length, 3);
|
||||
assert.ok(modePreferences?.SZ?.some((p) => !p.preference));
|
||||
},
|
||||
);
|
||||
|
||||
PrivateUserNoteSorting.before.each(async () => {
|
||||
await Test.database.insertUsers(8);
|
||||
|
||||
await createGroup([1]);
|
||||
await createGroup([2]);
|
||||
await createGroup([3]);
|
||||
await createGroup([4]);
|
||||
await createGroup([5]);
|
||||
await createGroup([6, 7]);
|
||||
await createGroup([8]);
|
||||
|
||||
await db
|
||||
.insertInto("GroupMatch")
|
||||
.values({ alphaGroupId: 2, bravoGroupId: 3 })
|
||||
.execute();
|
||||
expect(modePreferences?.SZ?.length).toBe(3);
|
||||
expect(modePreferences?.SZ?.some((p) => !p.preference)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
PrivateUserNoteSorting.after.each(() => {
|
||||
Test.database.reset();
|
||||
});
|
||||
describe("Private user note sorting", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(8);
|
||||
|
||||
const lookingLoader = Test.wrappedLoader<SerializeFrom<typeof loader>>({
|
||||
loader,
|
||||
});
|
||||
const matchAction = Test.wrappedAction<typeof matchSchema>({
|
||||
action: rawMatchAction,
|
||||
params: { id: "1" },
|
||||
});
|
||||
await createGroup([1]);
|
||||
await createGroup([2]);
|
||||
await createGroup([3]);
|
||||
await createGroup([4]);
|
||||
await createGroup([5]);
|
||||
await createGroup([6, 7]);
|
||||
await createGroup([8]);
|
||||
|
||||
PrivateUserNoteSorting("users with positive note sorted first", async () => {
|
||||
await matchAction(
|
||||
{
|
||||
_action: "ADD_PRIVATE_USER_NOTE",
|
||||
targetId: 5,
|
||||
sentiment: "POSITIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
);
|
||||
await db
|
||||
.insertInto("GroupMatch")
|
||||
.values({ alphaGroupId: 2, bravoGroupId: 3 })
|
||||
.execute();
|
||||
});
|
||||
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
assert.equal(data.groups.neutral[0].members![0].id, 5);
|
||||
});
|
||||
const lookingLoader = wrappedLoader<SerializeFrom<typeof loader>>({
|
||||
loader,
|
||||
});
|
||||
const matchAction = wrappedAction<typeof matchSchema>({
|
||||
action: rawMatchAction,
|
||||
params: { id: "1" },
|
||||
});
|
||||
|
||||
PrivateUserNoteSorting("users with negative note sorted last", async () => {
|
||||
await matchAction(
|
||||
{
|
||||
_action: "ADD_PRIVATE_USER_NOTE",
|
||||
targetId: 5,
|
||||
sentiment: "NEGATIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
);
|
||||
test("users with positive note sorted first", async () => {
|
||||
await matchAction(
|
||||
{
|
||||
_action: "ADD_PRIVATE_USER_NOTE",
|
||||
targetId: 5,
|
||||
sentiment: "POSITIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
);
|
||||
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
|
||||
assert.equal(
|
||||
data.groups.neutral[data.groups.neutral.length - 1].members![0].id,
|
||||
5,
|
||||
);
|
||||
});
|
||||
expect(data.groups.neutral[0].members![0].id).toBe(5);
|
||||
});
|
||||
|
||||
PrivateUserNoteSorting(
|
||||
"group with both negative and positive sentiment sorted last",
|
||||
async () => {
|
||||
test("users with negative note sorted last", async () => {
|
||||
await matchAction(
|
||||
{
|
||||
_action: "ADD_PRIVATE_USER_NOTE",
|
||||
targetId: 5,
|
||||
sentiment: "NEGATIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
);
|
||||
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
|
||||
expect(
|
||||
data.groups.neutral[data.groups.neutral.length - 1].members![0].id,
|
||||
).toBe(5);
|
||||
});
|
||||
|
||||
test("group with both negative and positive sentiment sorted last", async () => {
|
||||
await matchAction(
|
||||
{
|
||||
_action: "ADD_PRIVATE_USER_NOTE",
|
||||
@@ -260,13 +263,10 @@ PrivateUserNoteSorting(
|
||||
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
|
||||
assert.ok(
|
||||
expect(
|
||||
data.groups.neutral[data.groups.neutral.length - 1].members?.some(
|
||||
(m) => m.id === 6,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SendouQMatchCreation.run();
|
||||
PrivateUserNoteSorting.run();
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
team,
|
||||
user,
|
||||
isInTeam: Boolean(
|
||||
(await UserRepository.findByIdentifier(String(user.id)))?.team,
|
||||
(await UserRepository.findProfileByIdentifier(String(user.id)))?.team,
|
||||
),
|
||||
}) === "VALID",
|
||||
"Invite code is invalid",
|
||||
@@ -68,7 +68,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
team,
|
||||
user,
|
||||
isInTeam: Boolean(
|
||||
(await UserRepository.findByIdentifier(String(user.id)))?.team,
|
||||
(await UserRepository.findProfileByIdentifier(String(user.id)))?.team,
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { userTopPlacements } from "./queries/userTopPlacements.server";
|
||||
@@ -1,54 +0,0 @@
|
||||
import { sql } from "~/db/sql";
|
||||
import type { XRankPlacement } from "~/db/types";
|
||||
import type { ModeShort } from "~/modules/in-game-lists";
|
||||
|
||||
const smt = sql.prepare(/* sql */ `
|
||||
select
|
||||
"power",
|
||||
"rank",
|
||||
"mode",
|
||||
"playerId"
|
||||
from "XRankPlacement"
|
||||
left join "SplatoonPlayer" on "SplatoonPlayer"."id" = "XRankPlacement"."playerId"
|
||||
left join "User" on "User"."id" = "SplatoonPlayer"."userId"
|
||||
where
|
||||
"User"."id" = @userId
|
||||
`);
|
||||
|
||||
type Row = Pick<XRankPlacement, "power" | "rank" | "mode" | "playerId">;
|
||||
export const userTopPlacements = (userId: number) => {
|
||||
const rows = smt.all({ userId }) as Row[];
|
||||
|
||||
const playerId = rows[0]?.playerId;
|
||||
|
||||
return { topPlacements: resolveTopPlacements(rows), playerId };
|
||||
};
|
||||
|
||||
type TopPlacements = Partial<
|
||||
Record<ModeShort, Pick<XRankPlacement, "power" | "rank">>
|
||||
>;
|
||||
|
||||
function resolveTopPlacements(placements: Row[]) {
|
||||
const result: TopPlacements = {};
|
||||
|
||||
for (const { mode, power, rank } of placements) {
|
||||
let current = result[mode];
|
||||
|
||||
if (!current) {
|
||||
result[mode] = { power, rank };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.rank > rank) {
|
||||
const newResult = { ...current, rank };
|
||||
result[mode] = newResult;
|
||||
current = newResult;
|
||||
}
|
||||
|
||||
if (current.power < power) {
|
||||
result[mode] = { ...current, power };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SerializeFrom } from "@remix-run/node";
|
||||
import type { TFunction } from "i18next";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "~/components/Button";
|
||||
@@ -66,7 +67,11 @@ export function OrganizerMatchMapListDialog({
|
||||
{number}) {t(`game-misc:MODE_LONG_${map.mode}`)} on{" "}
|
||||
{t(`game-misc:STAGE_${map.stageId}`)}{" "}
|
||||
<span className="text-lighter text-xs italic ml-1">
|
||||
{pickInfoText({ t, teams: [teamOne, teamTwo], map })}
|
||||
{pickInfoText({
|
||||
t: t as unknown as TFunction<["tournament"]>,
|
||||
teams: [teamOne, teamTwo],
|
||||
map,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SerializeFrom } from "@remix-run/node";
|
||||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import type { TFunction } from "i18next";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "~/components/Image";
|
||||
@@ -378,7 +379,13 @@ function FancyStageBanner({
|
||||
{t(`game-misc:STAGE_${stage.stageId}`)}
|
||||
</span>
|
||||
</h4>
|
||||
<h4>{pickInfoText({ t, teams, map: stage })}</h4>
|
||||
<h4>
|
||||
{pickInfoText({
|
||||
t: t as unknown as TFunction<["tournament"]>,
|
||||
teams,
|
||||
map: stage,
|
||||
})}
|
||||
</h4>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { Match } from "~/modules/brackets-model";
|
||||
import { Tournament } from "./Tournament";
|
||||
import {
|
||||
@@ -9,152 +8,151 @@ import {
|
||||
PADDLING_POOL_257,
|
||||
} from "./tests/mocks";
|
||||
|
||||
const FollowUp = suite("Follow-up bracket progression");
|
||||
describe("Follow-up bracket progression", () => {
|
||||
const tournamentPP257 = new Tournament(PADDLING_POOL_257());
|
||||
const tournamentPP255 = new Tournament(PADDLING_POOL_255());
|
||||
const tournamentITZ32 = new Tournament(IN_THE_ZONE_32());
|
||||
|
||||
const tournamentPP257 = new Tournament(PADDLING_POOL_257());
|
||||
const tournamentPP255 = new Tournament(PADDLING_POOL_255());
|
||||
const tournamentITZ32 = new Tournament(IN_THE_ZONE_32());
|
||||
test("correct amount of teams in the top cut", () => {
|
||||
expect(tournamentPP257.brackets[1].seeding?.length).toBe(18);
|
||||
});
|
||||
|
||||
FollowUp("correct amount of teams in the top cut", () => {
|
||||
assert.equal(tournamentPP257.brackets[1].seeding?.length, 18);
|
||||
});
|
||||
|
||||
FollowUp("includes correct teams in the top cut", () => {
|
||||
for (const tournamentTeamId of [892, 882, 881]) {
|
||||
assert.ok(
|
||||
tournamentPP257.brackets[1].seeding?.some(
|
||||
(team) => team === tournamentTeamId,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
FollowUp("underground bracket includes a checked in team", () => {
|
||||
assert.ok(tournamentPP257.brackets[2].seeding?.some((team) => team === 902));
|
||||
});
|
||||
|
||||
FollowUp("underground bracket doesn't include a non checked in team", () => {
|
||||
assert.ok(tournamentPP257.brackets[2].seeding?.some((team) => team === 902));
|
||||
});
|
||||
|
||||
FollowUp("underground bracket includes checked in teams (DE->SE)", () => {
|
||||
assert.equal(tournamentITZ32.brackets[1].seeding?.length, 4);
|
||||
});
|
||||
|
||||
const AMOUNT_OF_WORSE_VS_BEST = 5;
|
||||
const AMOUNT_OF_BEST_VS_BEST = 1;
|
||||
const AMOUNT_OF_WORSE_VS_WORSE = 2;
|
||||
|
||||
FollowUp("correct seed distribution in the top cut", () => {
|
||||
const rrPlacements = tournamentPP257.brackets[0].standings;
|
||||
|
||||
let ACTUAL_AMOUNT_OF_WORSE_VS_BEST = 0;
|
||||
let ACTUAL_AMOUNT_OF_BEST_VS_BEST = 0;
|
||||
let ACTUAL_AMOUNT_OF_WORSE_VS_WORSE = 0;
|
||||
for (const match of tournamentPP257.brackets[1].data.match) {
|
||||
const opponent1 = rrPlacements.find(
|
||||
(placement) => placement.team.id === match.opponent1?.id,
|
||||
);
|
||||
const opponent2 = rrPlacements.find(
|
||||
(placement) => placement.team.id === match.opponent2?.id,
|
||||
);
|
||||
|
||||
if (!opponent1 || !opponent2) {
|
||||
continue;
|
||||
test("includes correct teams in the top cut", () => {
|
||||
for (const tournamentTeamId of [892, 882, 881]) {
|
||||
expect(
|
||||
tournamentPP257.brackets[1].seeding?.some(
|
||||
(team) => team === tournamentTeamId,
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
const placementDiff = opponent1.placement - opponent2.placement;
|
||||
if (placementDiff === 0 && opponent1.placement === 1) {
|
||||
ACTUAL_AMOUNT_OF_BEST_VS_BEST++;
|
||||
} else if (placementDiff === 0 && opponent1.placement === 10) {
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_WORSE++;
|
||||
} else {
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_BEST++;
|
||||
}
|
||||
}
|
||||
test("underground bracket includes a checked in team", () => {
|
||||
expect(
|
||||
tournamentPP257.brackets[2].seeding?.some((team) => team === 902),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_BEST,
|
||||
AMOUNT_OF_WORSE_VS_BEST,
|
||||
"Amount of worse vs best is incorrect",
|
||||
);
|
||||
assert.equal(
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_WORSE,
|
||||
AMOUNT_OF_WORSE_VS_WORSE,
|
||||
"Amount of worse vs worse is incorrect",
|
||||
);
|
||||
assert.equal(
|
||||
ACTUAL_AMOUNT_OF_BEST_VS_BEST,
|
||||
AMOUNT_OF_BEST_VS_BEST,
|
||||
"Amount of best vs best is incorrect",
|
||||
);
|
||||
});
|
||||
test("underground bracket doesn't include a non checked in team", () => {
|
||||
expect(
|
||||
tournamentPP257.brackets[2].seeding?.some((team) => team === 902),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
const validateNoRematches = (rrMatches: Match[], topCutMatches: Match[]) => {
|
||||
for (const topCutMatch of topCutMatches) {
|
||||
if (!topCutMatch.opponent1?.id || !topCutMatch.opponent2?.id) {
|
||||
continue;
|
||||
}
|
||||
test("underground bracket includes checked in teams (DE->SE)", () => {
|
||||
expect(tournamentITZ32.brackets[1].seeding?.length).toBe(4);
|
||||
});
|
||||
|
||||
for (const rrMatch of rrMatches) {
|
||||
if (
|
||||
rrMatch.opponent1?.id === topCutMatch.opponent1.id &&
|
||||
rrMatch.opponent2?.id === topCutMatch.opponent2.id
|
||||
) {
|
||||
throw new Error(
|
||||
`Rematch detected: ${rrMatch.opponent1.id} vs ${rrMatch.opponent2.id}`,
|
||||
);
|
||||
const AMOUNT_OF_WORSE_VS_BEST = 5;
|
||||
const AMOUNT_OF_BEST_VS_BEST = 1;
|
||||
const AMOUNT_OF_WORSE_VS_WORSE = 2;
|
||||
|
||||
test("correct seed distribution in the top cut", () => {
|
||||
const rrPlacements = tournamentPP257.brackets[0].standings;
|
||||
|
||||
let ACTUAL_AMOUNT_OF_WORSE_VS_BEST = 0;
|
||||
let ACTUAL_AMOUNT_OF_BEST_VS_BEST = 0;
|
||||
let ACTUAL_AMOUNT_OF_WORSE_VS_WORSE = 0;
|
||||
for (const match of tournamentPP257.brackets[1].data.match) {
|
||||
const opponent1 = rrPlacements.find(
|
||||
(placement) => placement.team.id === match.opponent1?.id,
|
||||
);
|
||||
const opponent2 = rrPlacements.find(
|
||||
(placement) => placement.team.id === match.opponent2?.id,
|
||||
);
|
||||
|
||||
if (!opponent1 || !opponent2) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
rrMatch.opponent1?.id === topCutMatch.opponent2.id &&
|
||||
rrMatch.opponent2?.id === topCutMatch.opponent1.id
|
||||
) {
|
||||
throw new Error(
|
||||
`Rematch detected: ${rrMatch.opponent1.id} vs ${rrMatch.opponent2.id}`,
|
||||
);
|
||||
|
||||
const placementDiff = opponent1.placement - opponent2.placement;
|
||||
if (placementDiff === 0 && opponent1.placement === 1) {
|
||||
ACTUAL_AMOUNT_OF_BEST_VS_BEST++;
|
||||
} else if (placementDiff === 0 && opponent1.placement === 10) {
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_WORSE++;
|
||||
} else {
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_BEST++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
FollowUp("avoids rematches in RR -> SE (PP 257)", () => {
|
||||
const rrMatches = tournamentPP257.brackets[0].data.match;
|
||||
const topCutMatches = tournamentPP257.brackets[1].data.match;
|
||||
expect(
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_BEST,
|
||||
"Amount of worse vs best is incorrect",
|
||||
).toBe(AMOUNT_OF_WORSE_VS_BEST);
|
||||
expect(
|
||||
ACTUAL_AMOUNT_OF_WORSE_VS_WORSE,
|
||||
"Amount of worse vs worse is incorrect",
|
||||
).toBe(AMOUNT_OF_WORSE_VS_WORSE);
|
||||
expect(
|
||||
ACTUAL_AMOUNT_OF_BEST_VS_BEST,
|
||||
"Amount of best vs best is incorrect",
|
||||
).toBe(AMOUNT_OF_BEST_VS_BEST);
|
||||
});
|
||||
|
||||
validateNoRematches(rrMatches, topCutMatches);
|
||||
});
|
||||
const validateNoRematches = (rrMatches: Match[], topCutMatches: Match[]) => {
|
||||
for (const topCutMatch of topCutMatches) {
|
||||
if (!topCutMatch.opponent1?.id || !topCutMatch.opponent2?.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FollowUp("avoids rematches in RR -> SE (PP 255)", () => {
|
||||
const rrMatches = tournamentPP255.brackets[0].data.match;
|
||||
const topCutMatches = tournamentPP255.brackets[1].data.match;
|
||||
for (const rrMatch of rrMatches) {
|
||||
if (
|
||||
rrMatch.opponent1?.id === topCutMatch.opponent1.id &&
|
||||
rrMatch.opponent2?.id === topCutMatch.opponent2.id
|
||||
) {
|
||||
throw new Error(
|
||||
`Rematch detected: ${rrMatch.opponent1.id} vs ${rrMatch.opponent2.id}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
rrMatch.opponent1?.id === topCutMatch.opponent2.id &&
|
||||
rrMatch.opponent2?.id === topCutMatch.opponent1.id
|
||||
) {
|
||||
throw new Error(
|
||||
`Rematch detected: ${rrMatch.opponent1.id} vs ${rrMatch.opponent2.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
validateNoRematches(rrMatches, topCutMatches);
|
||||
});
|
||||
test("avoids rematches in RR -> SE (PP 257)", () => {
|
||||
const rrMatches = tournamentPP257.brackets[0].data.match;
|
||||
const topCutMatches = tournamentPP257.brackets[1].data.match;
|
||||
|
||||
FollowUp("avoids rematches in RR -> SE (PP 255) - only minimum swap", () => {
|
||||
const oldTopCutMatches = PADDLING_POOL_255_TOP_CUT_INITIAL_MATCHES();
|
||||
const newTopCutMatches = tournamentPP255.brackets[1].data.match;
|
||||
validateNoRematches(rrMatches, topCutMatches);
|
||||
});
|
||||
|
||||
let different = 0;
|
||||
test("avoids rematches in RR -> SE (PP 255)", () => {
|
||||
const rrMatches = tournamentPP255.brackets[0].data.match;
|
||||
const topCutMatches = tournamentPP255.brackets[1].data.match;
|
||||
|
||||
for (const match of oldTopCutMatches) {
|
||||
if (!match.opponent1?.id || !match.opponent2?.id) {
|
||||
continue;
|
||||
validateNoRematches(rrMatches, topCutMatches);
|
||||
});
|
||||
|
||||
test("avoids rematches in RR -> SE (PP 255) - only minimum swap", () => {
|
||||
const oldTopCutMatches = PADDLING_POOL_255_TOP_CUT_INITIAL_MATCHES();
|
||||
const newTopCutMatches = tournamentPP255.brackets[1].data.match;
|
||||
|
||||
let different = 0;
|
||||
|
||||
for (const match of oldTopCutMatches) {
|
||||
if (!match.opponent1?.id || !match.opponent2?.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newMatch = newTopCutMatches.find(
|
||||
(m) =>
|
||||
m.opponent1?.id === match.opponent1.id &&
|
||||
m.opponent2?.id === match.opponent2.id,
|
||||
);
|
||||
|
||||
if (!newMatch) {
|
||||
different++;
|
||||
}
|
||||
}
|
||||
|
||||
const newMatch = newTopCutMatches.find(
|
||||
(m) =>
|
||||
m.opponent1?.id === match.opponent1.id &&
|
||||
m.opponent2?.id === match.opponent2.id,
|
||||
);
|
||||
|
||||
if (!newMatch) {
|
||||
different++;
|
||||
}
|
||||
}
|
||||
|
||||
// 1 team should get swapped meaning two matches are now different
|
||||
assert.equal(different, 2, "Amount of different matches is incorrect");
|
||||
// 1 team should get swapped meaning two matches are now different
|
||||
expect(different, "Amount of different matches is incorrect").toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
FollowUp.run();
|
||||
|
||||
@@ -492,7 +492,7 @@ export class Match {
|
||||
|
||||
update() {
|
||||
match_updateStm.run({
|
||||
id: this.id,
|
||||
id: this.id ?? null,
|
||||
roundId: this.roundId,
|
||||
stageId: this.stageId,
|
||||
groupId: this.groupId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import shuffle from "just-shuffle";
|
||||
import type { Rating } from "openskill/dist/types";
|
||||
import type { Rating } from "node_modules/openskill/dist/types";
|
||||
import type {
|
||||
MapResult,
|
||||
PlayerResult,
|
||||
|
||||
@@ -1,364 +1,338 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { ordinal, rating } from "openskill";
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { AllMatchResult } from "../queries/allMatchResultsByTournamentId.server";
|
||||
import type { TournamentDataTeam } from "./Tournament.server";
|
||||
import { tournamentSummary } from "./summarizer.server";
|
||||
|
||||
const TournamentSummary = suite("tournamentSummary()");
|
||||
|
||||
const createTeam = (teamId: number, userIds: number[]): TournamentDataTeam => ({
|
||||
checkIns: [],
|
||||
createdAt: 0,
|
||||
id: teamId,
|
||||
inviteCode: null,
|
||||
mapPool: [],
|
||||
members: userIds.map((userId) => ({
|
||||
country: null,
|
||||
customUrl: null,
|
||||
discordAvatar: null,
|
||||
discordId: "123",
|
||||
username: "test",
|
||||
inGameName: "test",
|
||||
twitch: null,
|
||||
isOwner: 0,
|
||||
plusTier: null,
|
||||
describe("tournamentSummary()", () => {
|
||||
const createTeam = (
|
||||
teamId: number,
|
||||
userIds: number[],
|
||||
): TournamentDataTeam => ({
|
||||
checkIns: [],
|
||||
createdAt: 0,
|
||||
userId,
|
||||
})),
|
||||
name: `Team ${teamId}`,
|
||||
prefersNotToHost: 0,
|
||||
droppedOut: 0,
|
||||
noScreen: 0,
|
||||
team: null,
|
||||
seed: 1,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
});
|
||||
|
||||
function summarize({ results }: { results?: AllMatchResult[] } = {}) {
|
||||
return tournamentSummary({
|
||||
finalStandings: [
|
||||
{
|
||||
placement: 1,
|
||||
team: createTeam(1, [1, 2, 3, 4]),
|
||||
},
|
||||
{
|
||||
placement: 2,
|
||||
team: createTeam(2, [5, 6, 7, 8]),
|
||||
},
|
||||
{
|
||||
placement: 3,
|
||||
team: createTeam(3, [9, 10, 11, 12]),
|
||||
},
|
||||
{
|
||||
placement: 4,
|
||||
team: createTeam(4, [13, 14, 15, 16]),
|
||||
},
|
||||
],
|
||||
results: results ?? [
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
members: [
|
||||
{ userId: 1 },
|
||||
{ userId: 2 },
|
||||
{ userId: 3 },
|
||||
{ userId: 4 },
|
||||
{ userId: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
members: [{ userId: 5 }, { userId: 6 }, { userId: 7 }, { userId: 8 }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
members: [
|
||||
{ userId: 9 },
|
||||
{ userId: 10 },
|
||||
{ userId: 11 },
|
||||
{ userId: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
members: [
|
||||
{ userId: 13 },
|
||||
{ userId: 14 },
|
||||
{ userId: 15 },
|
||||
{ userId: 16 },
|
||||
],
|
||||
},
|
||||
],
|
||||
queryCurrentTeamRating: () => rating(),
|
||||
queryCurrentUserRating: () => rating(),
|
||||
queryTeamPlayerRatingAverage: () => rating(),
|
||||
id: teamId,
|
||||
inviteCode: null,
|
||||
mapPool: [],
|
||||
members: userIds.map((userId) => ({
|
||||
country: null,
|
||||
customUrl: null,
|
||||
discordAvatar: null,
|
||||
discordId: "123",
|
||||
username: "test",
|
||||
inGameName: "test",
|
||||
twitch: null,
|
||||
isOwner: 0,
|
||||
plusTier: null,
|
||||
createdAt: 0,
|
||||
userId,
|
||||
})),
|
||||
name: `Team ${teamId}`,
|
||||
prefersNotToHost: 0,
|
||||
droppedOut: 0,
|
||||
noScreen: 0,
|
||||
team: null,
|
||||
seed: 1,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
});
|
||||
}
|
||||
|
||||
TournamentSummary("calculates final standings", () => {
|
||||
const summary = summarize();
|
||||
function summarize({ results }: { results?: AllMatchResult[] } = {}) {
|
||||
return tournamentSummary({
|
||||
finalStandings: [
|
||||
{
|
||||
placement: 1,
|
||||
team: createTeam(1, [1, 2, 3, 4]),
|
||||
},
|
||||
{
|
||||
placement: 2,
|
||||
team: createTeam(2, [5, 6, 7, 8]),
|
||||
},
|
||||
{
|
||||
placement: 3,
|
||||
team: createTeam(3, [9, 10, 11, 12]),
|
||||
},
|
||||
{
|
||||
placement: 4,
|
||||
team: createTeam(4, [13, 14, 15, 16]),
|
||||
},
|
||||
],
|
||||
results: results ?? [
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
members: [
|
||||
{ userId: 1 },
|
||||
{ userId: 2 },
|
||||
{ userId: 3 },
|
||||
{ userId: 4 },
|
||||
{ userId: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
members: [{ userId: 5 }, { userId: 6 }, { userId: 7 }, { userId: 8 }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
members: [
|
||||
{ userId: 9 },
|
||||
{ userId: 10 },
|
||||
{ userId: 11 },
|
||||
{ userId: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
members: [
|
||||
{ userId: 13 },
|
||||
{ userId: 14 },
|
||||
{ userId: 15 },
|
||||
{ userId: 16 },
|
||||
],
|
||||
},
|
||||
],
|
||||
queryCurrentTeamRating: () => rating(),
|
||||
queryCurrentUserRating: () => rating(),
|
||||
queryTeamPlayerRatingAverage: () => rating(),
|
||||
});
|
||||
}
|
||||
|
||||
// each player of each team should have one result
|
||||
assert.equal(summary.tournamentResults.length, 4 * 4);
|
||||
});
|
||||
|
||||
TournamentSummary(
|
||||
"winners skill should go up, losers skill should go down",
|
||||
() => {
|
||||
test("calculates final standings", () => {
|
||||
const summary = summarize();
|
||||
expect(summary.tournamentResults.length).toBe(4 * 4);
|
||||
});
|
||||
|
||||
test("winners skill should go up, losers skill should go down", () => {
|
||||
const summary = summarize();
|
||||
const winnerSkill = summary.skills.find((s) => s.userId === 1);
|
||||
const loserSkill = summary.skills.find((s) => s.userId === 5);
|
||||
|
||||
assert.ok(winnerSkill);
|
||||
assert.ok(loserSkill);
|
||||
|
||||
assert.ok(ordinal(winnerSkill) > ordinal(loserSkill));
|
||||
},
|
||||
);
|
||||
|
||||
const resultsWith20: AllMatchResult[] = [
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 20, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 20, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
TournamentSummary("winning more than once makes the skill go up more", () => {
|
||||
const summary = summarize({
|
||||
results: resultsWith20,
|
||||
invariant(winnerSkill, "winnerSkill should be defined");
|
||||
invariant(loserSkill, "loserSkill should be defined");
|
||||
expect(ordinal(winnerSkill)).toBeGreaterThan(ordinal(loserSkill));
|
||||
});
|
||||
|
||||
const twoTimeWinnerSkill = summary.skills.find((s) => s.userId === 1);
|
||||
const oneTimeWinnerSkill = summary.skills.find((s) => s.userId === 2);
|
||||
|
||||
assert.ok(twoTimeWinnerSkill);
|
||||
assert.ok(oneTimeWinnerSkill);
|
||||
|
||||
assert.ok(ordinal(twoTimeWinnerSkill) > ordinal(oneTimeWinnerSkill));
|
||||
});
|
||||
|
||||
TournamentSummary("calculates team skills (many rosters for same team)", () => {
|
||||
const summary = summarize({
|
||||
results: resultsWith20,
|
||||
});
|
||||
|
||||
const teamOneRosterOne = summary.skills.find(
|
||||
(s) => s.identifier === "1-2-3-4",
|
||||
);
|
||||
const teamOneRosterTwo = summary.skills.find(
|
||||
(s) => s.identifier === "1-3-4-20",
|
||||
);
|
||||
|
||||
assert.ok(teamOneRosterOne);
|
||||
assert.ok(teamOneRosterTwo);
|
||||
});
|
||||
|
||||
const resultsWithSubbedRoster: AllMatchResult[] = [
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
const resultsWith20: AllMatchResult[] = [
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 2,
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 0,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 20, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 1,
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 20, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 20, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
TournamentSummary(
|
||||
"In the case of sub calculates skill based on the most common roster",
|
||||
() => {
|
||||
test("winning more than once makes the skill go up more", () => {
|
||||
const summary = summarize({
|
||||
results: resultsWithSubbedRoster,
|
||||
results: resultsWith20,
|
||||
});
|
||||
const twoTimeWinnerSkill = summary.skills.find((s) => s.userId === 1);
|
||||
const oneTimeWinnerSkill = summary.skills.find((s) => s.userId === 2);
|
||||
|
||||
invariant(twoTimeWinnerSkill, "twoTimeWinnerSkill should be defined");
|
||||
invariant(oneTimeWinnerSkill, "oneTimeWinnerSkill should be defined");
|
||||
expect(ordinal(twoTimeWinnerSkill)).toBeGreaterThan(
|
||||
ordinal(oneTimeWinnerSkill),
|
||||
);
|
||||
});
|
||||
|
||||
test("calculates team skills (many rosters for same team)", () => {
|
||||
const summary = summarize({
|
||||
results: resultsWith20,
|
||||
});
|
||||
const teamOneRosterOne = summary.skills.find(
|
||||
(s) => s.identifier === "1-2-3-4",
|
||||
);
|
||||
const teamOneRosterTwo = summary.skills.find(
|
||||
(s) => s.identifier === "1-3-4-20",
|
||||
);
|
||||
expect(teamOneRosterOne).toBeTruthy();
|
||||
expect(teamOneRosterTwo).toBeTruthy();
|
||||
});
|
||||
|
||||
assert.ok(teamOneRosterOne);
|
||||
assert.not.ok(teamOneRosterTwo);
|
||||
},
|
||||
);
|
||||
const resultsWithSubbedRoster: AllMatchResult[] = [
|
||||
{
|
||||
maps: [
|
||||
{
|
||||
mode: "SZ",
|
||||
stageId: 1,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 2,
|
||||
},
|
||||
{
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
userIds: [1, 20, 3, 4, 5, 6, 7, 8],
|
||||
winnerTeamId: 1,
|
||||
},
|
||||
],
|
||||
opponentOne: {
|
||||
id: 1,
|
||||
result: "win",
|
||||
score: 2,
|
||||
},
|
||||
opponentTwo: {
|
||||
id: 2,
|
||||
result: "loss",
|
||||
score: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
TournamentSummary(
|
||||
"In the case of sub calculates player results based on the most common roster",
|
||||
() => {
|
||||
test("In the case of sub calculates skill based on the most common roster", () => {
|
||||
const summary = summarize({
|
||||
results: resultsWithSubbedRoster,
|
||||
});
|
||||
const teamOneRosterOne = summary.skills.find(
|
||||
(s) => s.identifier === "1-2-3-4",
|
||||
);
|
||||
const teamOneRosterTwo = summary.skills.find(
|
||||
(s) => s.identifier === "1-3-4-20",
|
||||
);
|
||||
expect(teamOneRosterOne).toBeTruthy();
|
||||
expect(teamOneRosterTwo).toBeFalsy();
|
||||
});
|
||||
|
||||
assert.not.ok(
|
||||
test("In the case of sub calculates player results based on the most common roster", () => {
|
||||
const summary = summarize({
|
||||
results: resultsWithSubbedRoster,
|
||||
});
|
||||
expect(
|
||||
summary.playerResultDeltas.find(
|
||||
(p) =>
|
||||
p.ownerUserId === 5 &&
|
||||
p.otherUserId === 20 &&
|
||||
(p.setWins > 0 || p.setLosses > 0),
|
||||
),
|
||||
"player 5 should not have a result against player 20 (sub for only one game)",
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test("calculates results of mates", () => {
|
||||
const summary = summarize();
|
||||
const result = summary.playerResultDeltas.find(
|
||||
(r) => r.ownerUserId === 1 && r.otherUserId === 2,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TournamentSummary("calculates results of mates", () => {
|
||||
const summary = summarize();
|
||||
invariant(result, "result should be defined");
|
||||
expect(result.setWins).toBe(1);
|
||||
expect(result.setLosses).toBe(0);
|
||||
expect(result.mapWins).toBe(2);
|
||||
expect(result.mapLosses).toBe(0);
|
||||
expect(result.type).toBe("MATE");
|
||||
});
|
||||
|
||||
const result = summary.playerResultDeltas.find(
|
||||
(r) => r.ownerUserId === 1 && r.otherUserId === 2,
|
||||
);
|
||||
test("calculates results of opponents", () => {
|
||||
const summary = summarize();
|
||||
const result = summary.playerResultDeltas.find(
|
||||
(r) => r.ownerUserId === 1 && r.otherUserId === 5,
|
||||
);
|
||||
|
||||
assert.ok(result);
|
||||
invariant(result, "result should be defined");
|
||||
expect(result.setWins).toBe(1);
|
||||
expect(result.setLosses).toBe(0);
|
||||
expect(result.mapWins).toBe(2);
|
||||
expect(result.mapLosses).toBe(0);
|
||||
expect(result.type).toBe("ENEMY");
|
||||
});
|
||||
|
||||
assert.equal(result.setWins, 1);
|
||||
assert.equal(result.setLosses, 0);
|
||||
assert.equal(result.mapWins, 2);
|
||||
assert.equal(result.mapLosses, 0);
|
||||
assert.equal(result.type, "MATE");
|
||||
test("calculates results of opponents (losing side)", () => {
|
||||
const summary = summarize();
|
||||
const result = summary.playerResultDeltas.find(
|
||||
(r) => r.ownerUserId === 5 && r.otherUserId === 1,
|
||||
);
|
||||
|
||||
invariant(result, "result should be defined");
|
||||
expect(result.setWins).toBe(0);
|
||||
expect(result.setLosses).toBe(1);
|
||||
expect(result.mapWins).toBe(0);
|
||||
expect(result.mapLosses).toBe(2);
|
||||
expect(result.type).toBe("ENEMY");
|
||||
});
|
||||
|
||||
test("calculates map results", () => {
|
||||
const summary = summarize();
|
||||
const result = summary.mapResultDeltas.filter((r) => r.userId === 1);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.every((r) => r.wins === 1 && r.losses === 0)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
TournamentSummary("calculates results of opponents", () => {
|
||||
const summary = summarize();
|
||||
|
||||
const result = summary.playerResultDeltas.find(
|
||||
(r) => r.ownerUserId === 1 && r.otherUserId === 5,
|
||||
);
|
||||
|
||||
assert.ok(result);
|
||||
|
||||
assert.equal(result.setWins, 1);
|
||||
assert.equal(result.setLosses, 0);
|
||||
assert.equal(result.mapWins, 2);
|
||||
assert.equal(result.mapLosses, 0);
|
||||
assert.equal(result.type, "ENEMY");
|
||||
});
|
||||
|
||||
TournamentSummary("calculates results of opponents (losing side)", () => {
|
||||
const summary = summarize();
|
||||
|
||||
const result = summary.playerResultDeltas.find(
|
||||
(r) => r.ownerUserId === 5 && r.otherUserId === 1,
|
||||
);
|
||||
|
||||
assert.ok(result);
|
||||
|
||||
assert.equal(result.setWins, 0);
|
||||
assert.equal(result.setLosses, 1);
|
||||
assert.equal(result.mapWins, 0);
|
||||
assert.equal(result.mapLosses, 2);
|
||||
assert.equal(result.type, "ENEMY");
|
||||
});
|
||||
|
||||
TournamentSummary("calculates map results", () => {
|
||||
const summary = summarize();
|
||||
|
||||
const result = summary.mapResultDeltas.filter((r) => r.userId === 1);
|
||||
|
||||
assert.equal(result.length, 2);
|
||||
assert.ok(result.every((r) => r.wins === 1 && r.losses === 0));
|
||||
});
|
||||
|
||||
TournamentSummary.run();
|
||||
|
||||
@@ -119,10 +119,10 @@ export const addSummary = sql.transaction(
|
||||
mu: skill.mu,
|
||||
sigma: skill.sigma,
|
||||
ordinal: ordinal(skill),
|
||||
userId: skill.userId,
|
||||
identifier: skill.identifier,
|
||||
userId: skill.userId ?? null,
|
||||
identifier: skill.identifier ?? null,
|
||||
matchesCount: skill.matchesCount,
|
||||
season,
|
||||
season: season ?? null,
|
||||
}) as Skill;
|
||||
|
||||
if (insertedSkill.identifier) {
|
||||
@@ -142,7 +142,7 @@ export const addSummary = sql.transaction(
|
||||
userId: mapResultDelta.userId,
|
||||
wins: mapResultDelta.wins,
|
||||
losses: mapResultDelta.losses,
|
||||
season,
|
||||
season: season ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export const addSummary = sql.transaction(
|
||||
setWins: playerResultDelta.setWins,
|
||||
setLosses: playerResultDelta.setLosses,
|
||||
type: playerResultDelta.type,
|
||||
season,
|
||||
season: season ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { eventStream } from "remix-utils/sse/server";
|
||||
|
||||
import { tournamentIdFromParams } from "~/features/tournament";
|
||||
import { ignoreTransaction } from "~/utils/newrelic.server";
|
||||
import { emitter } from "../core/emitters.server";
|
||||
import { bracketSubscriptionKey } from "../tournament-bracket-utils";
|
||||
|
||||
export const loader = ({ request, params }: LoaderFunctionArgs) => {
|
||||
ignoreTransaction();
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
|
||||
return eventStream(request.signal, (send) => {
|
||||
|
||||
@@ -2,14 +2,12 @@ import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { eventStream } from "remix-utils/sse/server";
|
||||
|
||||
import { getUserId } from "~/features/auth/core/user.server";
|
||||
import { ignoreTransaction } from "~/utils/newrelic.server";
|
||||
import { parseParams } from "~/utils/remix";
|
||||
import { emitter } from "../core/emitters.server";
|
||||
import { matchPageParamsSchema } from "../tournament-bracket-schemas.server";
|
||||
import { matchSubscriptionKey } from "../tournament-bracket-utils";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
ignoreTransaction();
|
||||
const loggedInUser = await getUserId(request);
|
||||
const matchId = parseParams({
|
||||
params,
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
fillWithNullTillPowerOfTwo,
|
||||
mapCountPlayedInSetWithCertainty,
|
||||
} from "./tournament-bracket-utils";
|
||||
|
||||
const MapCountPlayedInSetWithCertainty = suite(
|
||||
"mapCountPlayedInSetWithCertainty()",
|
||||
);
|
||||
const FillWithNullTillPowerOfTwo = suite("fillWithNullTillPowerOfTwo()");
|
||||
|
||||
const mapCountParamsToResult: {
|
||||
bestOf: number;
|
||||
scores: [number, number];
|
||||
@@ -26,17 +20,15 @@ const mapCountParamsToResult: {
|
||||
{ bestOf: 7, scores: [2, 2], expected: 6 },
|
||||
];
|
||||
|
||||
for (const { bestOf, scores, expected } of mapCountParamsToResult) {
|
||||
MapCountPlayedInSetWithCertainty(
|
||||
`bestOf=${bestOf}, scores=${scores.join(",")} -> ${expected}`,
|
||||
() => {
|
||||
assert.equal(
|
||||
mapCountPlayedInSetWithCertainty({ bestOf, scores }),
|
||||
describe("mapCountPlayedInSetWithCertainty()", () => {
|
||||
for (const { bestOf, scores, expected } of mapCountParamsToResult) {
|
||||
test(`bestOf=${bestOf}, scores=${scores.join(",")} -> ${expected}`, () => {
|
||||
expect(mapCountPlayedInSetWithCertainty({ bestOf, scores })).toBe(
|
||||
expected,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const powerOfTwoParamsToResults: [
|
||||
amountOfTeams: number,
|
||||
@@ -50,19 +42,14 @@ const powerOfTwoParamsToResults: [
|
||||
[17, 15],
|
||||
];
|
||||
|
||||
for (const [amountOfTeams, expectedNullCount] of powerOfTwoParamsToResults) {
|
||||
FillWithNullTillPowerOfTwo(
|
||||
`amountOfTeams=${amountOfTeams} -> ${expectedNullCount}`,
|
||||
() => {
|
||||
assert.equal(
|
||||
describe("fillWithNullTillPowerOfTwo()", () => {
|
||||
for (const [amountOfTeams, expectedNullCount] of powerOfTwoParamsToResults) {
|
||||
test(`amountOfTeams=${amountOfTeams} -> ${expectedNullCount}`, () => {
|
||||
expect(
|
||||
fillWithNullTillPowerOfTwo(Array(amountOfTeams).fill("team")).filter(
|
||||
(x) => x === null,
|
||||
).length,
|
||||
expectedNullCount,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
MapCountPlayedInSetWithCertainty.run();
|
||||
FillWithNullTillPowerOfTwo.run();
|
||||
).toBe(expectedNullCount);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -180,7 +180,7 @@ export function pickInfoText({
|
||||
teams,
|
||||
}: {
|
||||
map?: { stageId: StageId; mode: ModeShort; source: TournamentMaplistSource };
|
||||
t: TFunction;
|
||||
t: TFunction<["tournament"]>;
|
||||
teams: [TournamentDataTeam, TournamentDataTeam];
|
||||
}) {
|
||||
if (!map) return "";
|
||||
|
||||
@@ -58,7 +58,7 @@ export function findSubsByTournamentId({
|
||||
tournamentId: number;
|
||||
userId?: number;
|
||||
}): SubByTournamentId[] {
|
||||
const rows = stm.all({ tournamentId, userId }) as any[];
|
||||
const rows = stm.all({ tournamentId, userId: userId ?? null }) as any[];
|
||||
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
|
||||
@@ -46,9 +46,9 @@ export const joinTeam = sql.transaction(
|
||||
checkOutTeam?: boolean;
|
||||
}) => {
|
||||
if (whatToDoWithPreviousTeam === "DELETE") {
|
||||
deleteTeamStm.run({ tournamentTeamId: previousTeamId });
|
||||
deleteTeamStm.run({ tournamentTeamId: previousTeamId ?? null });
|
||||
} else if (whatToDoWithPreviousTeam === "LEAVE") {
|
||||
deleteMemberStm.run({ tournamentTeamId: previousTeamId, userId });
|
||||
deleteMemberStm.run({ tournamentTeamId: previousTeamId ?? null, userId });
|
||||
}
|
||||
|
||||
if (!previousTeamId) {
|
||||
|
||||
@@ -132,7 +132,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
const tournament = await tournamentDataCached({ tournamentId, user });
|
||||
|
||||
const streams =
|
||||
tournament.data.stage.length > 0
|
||||
tournament.data.stage.length > 0 && !tournament.ctx.isFinalized
|
||||
? await streamsByTournamentId(tournament.ctx)
|
||||
: [];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ExpressionBuilder, FunctionModule } from "kysely";
|
||||
import type { ExpressionBuilder, FunctionModule, NotNull } from "kysely";
|
||||
import { sql } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { db, sql as dbDirect } from "~/db/sql";
|
||||
@@ -21,10 +21,11 @@ const identifierToUserIdQuery = (identifier: string) =>
|
||||
return eb("User.id", "=", parsedId);
|
||||
}
|
||||
|
||||
return eb.or([
|
||||
eb("User.discordId", "=", identifier),
|
||||
eb("User.customUrl", "=", identifier),
|
||||
]);
|
||||
if (/^\d+$/.test(identifier)) {
|
||||
return eb("User.discordId", "=", identifier);
|
||||
}
|
||||
|
||||
return eb("User.customUrl", "=", identifier);
|
||||
});
|
||||
|
||||
export function identifierToUserId(identifier: string) {
|
||||
@@ -55,36 +56,93 @@ export async function identifierToBuildFields(identifier: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function findByIdentifier(identifier: string) {
|
||||
export function findLayoutDataByIdentifier(
|
||||
identifier: string,
|
||||
loggedInUserId?: number,
|
||||
) {
|
||||
return identifierToUserIdQuery(identifier)
|
||||
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
|
||||
.select((eb) => [
|
||||
...COMMON_USER_FIELDS,
|
||||
"User.commissionText",
|
||||
"User.commissionsOpen",
|
||||
sql<Record<
|
||||
string,
|
||||
string
|
||||
> | null>`IIF(COALESCE("User"."patronTier", 0) >= 2, "User"."css", null)`.as(
|
||||
"css",
|
||||
),
|
||||
eb
|
||||
.selectFrom("TournamentResult")
|
||||
.whereRef("TournamentResult.userId", "=", "User.id")
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.as("tournamentResultsCount"),
|
||||
eb
|
||||
.selectFrom("CalendarEventResultPlayer")
|
||||
.whereRef("CalendarEventResultPlayer.userId", "=", "User.id")
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.as("calendarEventResultsCount"),
|
||||
eb
|
||||
.selectFrom("Build")
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.whereRef("Build.ownerId", "=", "User.id")
|
||||
.where((eb) =>
|
||||
eb.or(
|
||||
[
|
||||
eb("Build.private", "=", 0),
|
||||
loggedInUserId ? eb("Build.ownerId", "=", loggedInUserId) : null,
|
||||
].filter((filter) => filter !== null),
|
||||
),
|
||||
)
|
||||
.as("buildsCount"),
|
||||
eb
|
||||
.selectFrom("VideoMatchPlayer")
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.whereRef("VideoMatchPlayer.playerUserId", "=", "User.id")
|
||||
.as("vodsCount"),
|
||||
eb
|
||||
.selectFrom("Art")
|
||||
.innerJoin("ArtUserMetadata", "ArtUserMetadata.artId", "Art.id")
|
||||
.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
|
||||
.select(({ fn }) => fn.count<number>("Art.id").distinct().as("count"))
|
||||
.where((innerEb) =>
|
||||
innerEb.or([
|
||||
innerEb("Art.authorId", "=", sql.raw<any>("User.id")),
|
||||
innerEb("ArtUserMetadata.userId", "=", sql.raw<any>("User.id")),
|
||||
]),
|
||||
)
|
||||
.as("artCount"),
|
||||
])
|
||||
.$narrowType<{
|
||||
calendarEventResultsCount: NotNull;
|
||||
tournamentResultsCount: NotNull;
|
||||
buildsCount: NotNull;
|
||||
vodsCount: NotNull;
|
||||
artCount: NotNull;
|
||||
}>()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
export async function findProfileByIdentifier(
|
||||
identifier: string,
|
||||
forceShowDiscordUniqueName?: boolean,
|
||||
) {
|
||||
const row = await identifierToUserIdQuery(identifier)
|
||||
.innerJoin("PlusTier", "PlusTier.userId", "User.id")
|
||||
.select(({ eb }) => [
|
||||
"User.discordAvatar",
|
||||
"User.discordId",
|
||||
"User.discordName",
|
||||
"User.username",
|
||||
"User.customName",
|
||||
"User.showDiscordUniqueName",
|
||||
"User.discordUniqueName",
|
||||
"User.customUrl",
|
||||
"User.inGameName",
|
||||
"User.twitter",
|
||||
"User.country",
|
||||
"User.bio",
|
||||
"User.motionSens",
|
||||
"User.stickSens",
|
||||
"User.css",
|
||||
"User.twitch",
|
||||
"User.twitter",
|
||||
"User.youtubeId",
|
||||
"User.battlefy",
|
||||
"User.country",
|
||||
"User.bio",
|
||||
"User.motionSens",
|
||||
"User.stickSens",
|
||||
"User.inGameName",
|
||||
"User.customName",
|
||||
"User.discordName",
|
||||
"User.showDiscordUniqueName",
|
||||
"User.discordUniqueName",
|
||||
"User.favoriteBadgeId",
|
||||
"User.banned",
|
||||
"User.bannedReason",
|
||||
"User.commissionText",
|
||||
"User.commissionsOpen",
|
||||
"User.patronTier",
|
||||
"User.buildSorting",
|
||||
"PlusTier.tier as plusTier",
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
@@ -110,8 +168,71 @@ export function findByIdentifier(identifier: string) {
|
||||
])
|
||||
.whereRef("TeamMember.userId", "=", "User.id"),
|
||||
).as("team"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("BadgeOwner")
|
||||
.innerJoin("Badge", "Badge.id", "BadgeOwner.badgeId")
|
||||
.select(({ fn }) => [
|
||||
fn.count<number>("BadgeOwner.badgeId").as("count"),
|
||||
"Badge.id",
|
||||
"Badge.displayName",
|
||||
"Badge.code",
|
||||
"Badge.hue",
|
||||
])
|
||||
.whereRef("BadgeOwner.userId", "=", "User.id")
|
||||
.groupBy(["BadgeOwner.badgeId", "BadgeOwner.userId"]),
|
||||
).as("badges"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.innerJoin(
|
||||
"XRankPlacement",
|
||||
"XRankPlacement.playerId",
|
||||
"SplatoonPlayer.id",
|
||||
)
|
||||
.select(({ fn }) => [
|
||||
"XRankPlacement.mode",
|
||||
fn.max<number>("XRankPlacement.power").as("power"),
|
||||
fn.min<number>("XRankPlacement.rank").as("rank"),
|
||||
"XRankPlacement.playerId",
|
||||
])
|
||||
.whereRef("SplatoonPlayer.userId", "=", "User.id")
|
||||
.groupBy(["XRankPlacement.mode"]),
|
||||
).as("topPlacements"),
|
||||
])
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
// TODO: sort in SQL
|
||||
badges: row.badges.sort((a, b) => {
|
||||
if (a.id === row.favoriteBadgeId) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.id === row.favoriteBadgeId) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.id - b.id;
|
||||
}),
|
||||
discordUniqueName:
|
||||
forceShowDiscordUniqueName || row.showDiscordUniqueName
|
||||
? row.discordUniqueName
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function findBannedStatusByUserId(userId: number) {
|
||||
return db
|
||||
.selectFrom("User")
|
||||
.select(["User.banned", "User.bannedReason"])
|
||||
.where("User.id", "=", userId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
export function findLeanById(id: number) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Placement } from "~/components/Placement";
|
||||
import { Table } from "~/components/Table";
|
||||
import type { UserPageLoaderData } from "~/features/user-page/routes/u.$identifier";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import {
|
||||
calendarEventPage,
|
||||
@@ -11,9 +10,10 @@ import {
|
||||
tournamentTeamPage,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import type { UserResultsLoaderData } from "../loaders/u.$identifier.results.server";
|
||||
|
||||
export type UserResultsTableProps = {
|
||||
results: UserPageLoaderData["results"];
|
||||
results: UserResultsLoaderData["results"];
|
||||
id: string;
|
||||
hasHighlightCheckboxes?: boolean;
|
||||
};
|
||||
|
||||
22
app/features/user-page/loaders/u.$identifier.index.server.ts
Normal file
22
app/features/user-page/loaders/u.$identifier.index.server.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { getUserId } from "~/features/auth/core/user.server";
|
||||
import { userIsBanned } from "~/features/ban/core/banned.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { isAdmin } from "~/permissions";
|
||||
import { notFoundIfFalsy } from "~/utils/remix";
|
||||
|
||||
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
const loggedInUser = await getUserId(request);
|
||||
|
||||
const user = notFoundIfFalsy(
|
||||
await UserRepository.findProfileByIdentifier(params.identifier!),
|
||||
);
|
||||
|
||||
return {
|
||||
user,
|
||||
banned:
|
||||
isAdmin(loggedInUser) && userIsBanned(user.id)
|
||||
? await UserRepository.findBannedStatusByUserId(user.id)!
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { notFoundIfFalsy } from "~/utils/remix";
|
||||
|
||||
export type UserResultsLoaderData = SerializeFrom<typeof loader>;
|
||||
|
||||
// TODO: could further optimize by only loading highlighted results when needed
|
||||
export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const userId = notFoundIfFalsy(
|
||||
await UserRepository.identifierToUserId(params.identifier!),
|
||||
).id;
|
||||
|
||||
return {
|
||||
results: await UserRepository.findResultsByUserId(userId),
|
||||
};
|
||||
};
|
||||
14
app/features/user-page/loaders/u.$identifier.vods.server.ts
Normal file
14
app/features/user-page/loaders/u.$identifier.vods.server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { findVods } from "~/features/vods/queries/findVods.server";
|
||||
import { notFoundIfFalsy } from "~/utils/remix";
|
||||
|
||||
export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const userId = notFoundIfFalsy(
|
||||
await UserRepository.identifierToUserId(params.identifier!),
|
||||
).id;
|
||||
|
||||
return {
|
||||
vods: findVods({ userId }),
|
||||
};
|
||||
};
|
||||
@@ -98,7 +98,7 @@ export default function UserArtPage() {
|
||||
});
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const userPageData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
const hasBothArtMadeByAndMadeOf =
|
||||
data.arts.some((a) => a.author) && data.arts.some((a) => !a.author);
|
||||
@@ -122,7 +122,7 @@ export default function UserArtPage() {
|
||||
? t("art:pendingApproval", { count: data.unvalidatedArtCount })
|
||||
: null}
|
||||
</div>
|
||||
{userPageData.id === user?.id ? (
|
||||
{layoutData.user.id === user?.id ? (
|
||||
<AddArtButton isArtist={Boolean(user.isArtist)} />
|
||||
) : null}
|
||||
</div>
|
||||
@@ -189,9 +189,9 @@ export default function UserArtPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{userPageData.commissionsOpen || userPageData.commissionText ? (
|
||||
{layoutData.user.commissionsOpen || layoutData.user.commissionText ? (
|
||||
<div className="whitespace-pre-wrap">
|
||||
{userPageData.commissionsOpen ? (
|
||||
{layoutData.user.commissionsOpen ? (
|
||||
<span className="art__comms-header">
|
||||
{t("art:commissionsOpen")} {">>>"}
|
||||
</span>
|
||||
@@ -200,14 +200,14 @@ export default function UserArtPage() {
|
||||
{t("art:commissionsClosed")} {">>>"}
|
||||
</span>
|
||||
)}{" "}
|
||||
{userPageData.commissionText}
|
||||
{layoutData.user.commissionText}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ArtGrid
|
||||
arts={arts}
|
||||
enablePreview
|
||||
canEdit={userPageData.id === user?.id}
|
||||
canEdit={layoutData.user.id === user?.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ export const handle: SendouRouteHandle = {
|
||||
|
||||
export default function NewBuildPage() {
|
||||
const { buildToEdit } = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation(["builds", "common"]);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [abilities, setAbilities] =
|
||||
React.useState<BuildAbilitiesTupleWithUnknown>(
|
||||
@@ -72,7 +72,9 @@ export default function NewBuildPage() {
|
||||
<DescriptionTextarea />
|
||||
<ModeCheckboxes />
|
||||
<PrivateCheckbox />
|
||||
<SubmitButton className="mt-4">{t("actions.submit")}</SubmitButton>
|
||||
<SubmitButton className="mt-4">
|
||||
{t("common:actions.submit")}
|
||||
</SubmitButton>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -37,9 +37,9 @@ export const handle: SendouRouteHandle = {
|
||||
type BuildFilter = "ALL" | "PUBLIC" | "PRIVATE" | MainWeaponId;
|
||||
|
||||
export default function UserBuildsPage() {
|
||||
const { t } = useTranslation("builds");
|
||||
const { t } = useTranslation(["builds", "user"]);
|
||||
const user = useUser();
|
||||
const parentPageData = atOrError(useMatches(), -2).data as UserPageLoaderData;
|
||||
const layoutData = atOrError(useMatches(), -2).data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [weaponFilter, setWeaponFilter] = useSearchParamState<BuildFilter>({
|
||||
defaultValue: "ALL",
|
||||
@@ -50,7 +50,7 @@ export default function UserBuildsPage() {
|
||||
: mainWeaponIds.find((id) => id === Number(value)),
|
||||
});
|
||||
|
||||
const isOwnPage = user?.id === parentPageData.id;
|
||||
const isOwnPage = user?.id === layoutData.user.id;
|
||||
const [changingSorting, setChangingSorting] = useSearchParamState({
|
||||
defaultValue: false,
|
||||
name: "sorting",
|
||||
@@ -92,7 +92,7 @@ export default function UserBuildsPage() {
|
||||
</Button>
|
||||
{data.builds.length < BUILD.MAX_COUNT ? (
|
||||
<LinkButton
|
||||
to={userNewBuildPage(parentPageData)}
|
||||
to={userNewBuildPage(layoutData.user)}
|
||||
size="tiny"
|
||||
testId="new-build-button"
|
||||
icon={<PlusIcon />}
|
||||
@@ -143,7 +143,7 @@ function BuildsFilters({
|
||||
const { t } = useTranslation(["weapons", "builds"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const user = useUser();
|
||||
const parentPageData = atOrError(useMatches(), -2).data as UserPageLoaderData;
|
||||
const layoutData = atOrError(useMatches(), -2).data as UserPageLoaderData;
|
||||
|
||||
if (data.builds.length === 0) return null;
|
||||
|
||||
@@ -153,7 +153,7 @@ function BuildsFilters({
|
||||
const publicBuildsCount = data.builds.length - privateBuildsCount;
|
||||
|
||||
const showPublicPrivateFilters =
|
||||
user?.id === parentPageData.id && privateBuildsCount > 0;
|
||||
user?.id === layoutData.user.id && privateBuildsCount > 0;
|
||||
|
||||
const WeaponFilterMenuButton = React.forwardRef((props, ref) => (
|
||||
<Button
|
||||
@@ -315,7 +315,7 @@ function ChangeSortingDialog({ close }: { close: () => void }) {
|
||||
return (
|
||||
<div key={i} className="stack horizontal justify-between">
|
||||
<div className="font-bold">
|
||||
{i + 1}) {t(`user:builds.sorting.${sort}`)}
|
||||
{i + 1}) {t(`user:builds.sorting.${sort!}`)}
|
||||
</div>
|
||||
{(isLast && !canAddMoreSorting) ||
|
||||
(canAddMoreSorting && isSecondToLast) ? (
|
||||
|
||||
@@ -209,15 +209,21 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const { identifier } = userParamsSchema.parse(params);
|
||||
const userToBeEdited = notFoundIfFalsy(
|
||||
await UserRepository.findByIdentifier(identifier),
|
||||
await UserRepository.findLayoutDataByIdentifier(identifier),
|
||||
);
|
||||
if (user.id !== userToBeEdited.id) {
|
||||
throw redirect(userPage(userToBeEdited));
|
||||
}
|
||||
|
||||
const userProfile = (await UserRepository.findProfileByIdentifier(
|
||||
identifier,
|
||||
true,
|
||||
))!;
|
||||
|
||||
return {
|
||||
user: userProfile,
|
||||
favoriteBadgeId: user.favoriteBadgeId,
|
||||
discordUniqueName: userToBeEdited.discordUniqueName,
|
||||
discordUniqueName: userProfile.discordUniqueName,
|
||||
countries: Object.entries(countries)
|
||||
.map(([code, country]) => ({
|
||||
code,
|
||||
@@ -237,33 +243,33 @@ export default function UserEditPage() {
|
||||
const { t } = useTranslation(["common", "user"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const parentRouteData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="half-width">
|
||||
<Form className="u-edit__container" method="post">
|
||||
{canAddCustomizedColorsToUserProfile(user) ? (
|
||||
<CustomizedColorsInput initialColors={parentRouteData.css} />
|
||||
<CustomizedColorsInput initialColors={layoutData.css} />
|
||||
) : null}
|
||||
<CustomNameInput parentRouteData={parentRouteData} />
|
||||
<CustomUrlInput parentRouteData={parentRouteData} />
|
||||
<InGameNameInputs parentRouteData={parentRouteData} />
|
||||
<SensSelects parentRouteData={parentRouteData} />
|
||||
<BattlefyInput parentRouteData={parentRouteData} />
|
||||
<CountrySelect parentRouteData={parentRouteData} />
|
||||
<FavBadgeSelect parentRouteData={parentRouteData} />
|
||||
<WeaponPoolSelect parentRouteData={parentRouteData} />
|
||||
<BioTextarea initialValue={parentRouteData.bio} />
|
||||
<CustomNameInput />
|
||||
<CustomUrlInput parentRouteData={layoutData} />
|
||||
<InGameNameInputs />
|
||||
<SensSelects />
|
||||
<BattlefyInput />
|
||||
<CountrySelect />
|
||||
<FavBadgeSelect />
|
||||
<WeaponPoolSelect />
|
||||
<BioTextarea initialValue={data.user.bio} />
|
||||
{data.discordUniqueName ? (
|
||||
<ShowUniqueDiscordNameToggle parentRouteData={parentRouteData} />
|
||||
<ShowUniqueDiscordNameToggle />
|
||||
) : (
|
||||
<input type="hidden" name="showDiscordUniqueName" value="on" />
|
||||
)}
|
||||
{user?.isArtist ? (
|
||||
<>
|
||||
<CommissionsOpenToggle parentRouteData={parentRouteData} />
|
||||
<CommissionTextArea initialValue={parentRouteData.commissionText} />
|
||||
<CommissionsOpenToggle parentRouteData={layoutData} />
|
||||
<CommissionTextArea initialValue={layoutData.user.commissionText} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -300,18 +306,15 @@ function CustomUrlInput({
|
||||
id="customUrl"
|
||||
leftAddon="https://sendou.ink/u/"
|
||||
maxLength={USER.CUSTOM_URL_MAX_LENGTH}
|
||||
defaultValue={parentRouteData.customUrl ?? undefined}
|
||||
defaultValue={parentRouteData.user.customUrl ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomNameInput({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
function CustomNameInput() {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -320,25 +323,22 @@ function CustomNameInput({
|
||||
name="customName"
|
||||
id="customName"
|
||||
maxLength={USER.CUSTOM_NAME_MAX_LENGTH}
|
||||
defaultValue={parentRouteData.customName ?? undefined}
|
||||
defaultValue={data.user.customName ?? undefined}
|
||||
/>
|
||||
<FormMessage type="info">
|
||||
{t("user:forms.customName.info", {
|
||||
discordName: parentRouteData.discordName,
|
||||
discordName: data.user.discordName,
|
||||
})}
|
||||
</FormMessage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InGameNameInputs({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
function InGameNameInputs() {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const inGameNameParts = parentRouteData.inGameName?.split("#");
|
||||
const inGameNameParts = data.user.inGameName?.split("#");
|
||||
|
||||
return (
|
||||
<div className="stack items-start">
|
||||
@@ -369,12 +369,9 @@ const SENS_OPTIONS = [
|
||||
-50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35,
|
||||
40, 45, 50,
|
||||
];
|
||||
function SensSelects({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
function SensSelects() {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="stack horizontal md">
|
||||
@@ -383,7 +380,7 @@ function SensSelects({
|
||||
<select
|
||||
id="motionSens"
|
||||
name="motionSens"
|
||||
defaultValue={parentRouteData.motionSens ?? undefined}
|
||||
defaultValue={data.user.motionSens ?? undefined}
|
||||
className="u-edit__sens-select"
|
||||
>
|
||||
<option value="">{"-"}</option>
|
||||
@@ -400,7 +397,7 @@ function SensSelects({
|
||||
<select
|
||||
id="stickSens"
|
||||
name="stickSens"
|
||||
defaultValue={parentRouteData.stickSens ?? undefined}
|
||||
defaultValue={data.user.stickSens ?? undefined}
|
||||
className="u-edit__sens-select"
|
||||
>
|
||||
<option value="">{"-"}</option>
|
||||
@@ -415,11 +412,7 @@ function SensSelects({
|
||||
);
|
||||
}
|
||||
|
||||
function CountrySelect({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
function CountrySelect() {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
@@ -430,7 +423,7 @@ function CountrySelect({
|
||||
className="u-edit__country-select"
|
||||
name="country"
|
||||
id="country"
|
||||
defaultValue={parentRouteData.country ?? ""}
|
||||
defaultValue={data.user.country ?? ""}
|
||||
>
|
||||
<option value="" />
|
||||
{data.countries.map((country) => (
|
||||
@@ -443,12 +436,9 @@ function CountrySelect({
|
||||
);
|
||||
}
|
||||
|
||||
function BattlefyInput({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
function BattlefyInput() {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -457,7 +447,7 @@ function BattlefyInput({
|
||||
name="battlefy"
|
||||
id="battlefy"
|
||||
maxLength={USER.BATTLEFY_MAX_LENGTH}
|
||||
defaultValue={parentRouteData.battlefy ?? undefined}
|
||||
defaultValue={data.user.battlefy ?? undefined}
|
||||
leftAddon="https://battlefy.com/users/"
|
||||
/>
|
||||
<FormMessage type="info">{t("user:forms.info.battlefy")}</FormMessage>
|
||||
@@ -465,12 +455,9 @@ function BattlefyInput({
|
||||
);
|
||||
}
|
||||
|
||||
function WeaponPoolSelect({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
const [weapons, setWeapons] = React.useState(parentRouteData.weapons);
|
||||
function WeaponPoolSelect() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [weapons, setWeapons] = React.useState(data.user.weapons);
|
||||
const { t } = useTranslation(["user"]);
|
||||
|
||||
const latestWeapon = weapons[weapons.length - 1];
|
||||
@@ -581,20 +568,16 @@ function BioTextarea({ initialValue }: { initialValue: User["bio"] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FavBadgeSelect({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
function FavBadgeSelect() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["user"]);
|
||||
|
||||
// doesn't make sense to select favorite badge
|
||||
// if user has no badges or only has 1 badge
|
||||
if (parentRouteData.badges.length < 2) return null;
|
||||
if (data.user.badges.length < 2) return null;
|
||||
|
||||
// user's current favorite badge is the initial value
|
||||
const initialBadge = parentRouteData.badges.find(
|
||||
const initialBadge = data.user.badges.find(
|
||||
(badge) => badge.id === data.favoriteBadgeId,
|
||||
);
|
||||
|
||||
@@ -607,7 +590,7 @@ function FavBadgeSelect({
|
||||
id="favoriteBadgeId"
|
||||
defaultValue={initialBadge?.id}
|
||||
>
|
||||
{parentRouteData.badges.map((badge) => (
|
||||
{data.user.badges.map((badge) => (
|
||||
<option key={badge.id} value={badge.id}>
|
||||
{`${badge.displayName}`}
|
||||
</option>
|
||||
@@ -620,15 +603,11 @@ function FavBadgeSelect({
|
||||
);
|
||||
}
|
||||
|
||||
function ShowUniqueDiscordNameToggle({
|
||||
parentRouteData,
|
||||
}: {
|
||||
parentRouteData: UserPageLoaderData;
|
||||
}) {
|
||||
function ShowUniqueDiscordNameToggle() {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [checked, setChecked] = React.useState(
|
||||
Boolean(parentRouteData.showDiscordUniqueName),
|
||||
Boolean(data.user.showDiscordUniqueName),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -657,7 +636,7 @@ function CommissionsOpenToggle({
|
||||
}) {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const [checked, setChecked] = React.useState(
|
||||
Boolean(parentRouteData.commissionsOpen),
|
||||
Boolean(parentRouteData.user.commissionsOpen),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link, useMatches } from "@remix-run/react";
|
||||
import { Link, useLoaderData, useMatches } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
@@ -11,6 +11,7 @@ import { TwitterIcon } from "~/components/icons/Twitter";
|
||||
import { YouTubeIcon } from "~/components/icons/YouTube";
|
||||
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
|
||||
import { modesShort } from "~/modules/in-game-lists";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { SendouRouteHandle } from "~/utils/remix";
|
||||
import { rawSensToString } from "~/utils/strings";
|
||||
@@ -24,71 +25,76 @@ import {
|
||||
} from "~/utils/urls";
|
||||
import type { UserPageLoaderData } from "./u.$identifier";
|
||||
|
||||
import { loader } from "../loaders/u.$identifier.index.server";
|
||||
export { loader };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "badges",
|
||||
};
|
||||
|
||||
export default function UserInfoPage() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<div className="u__container">
|
||||
<div className="u__avatar-container">
|
||||
<Avatar user={data} size="lg" className="u__avatar" />
|
||||
<Avatar user={layoutData.user} size="lg" className="u__avatar" />
|
||||
<div>
|
||||
<h2 className="u__name">
|
||||
<div>{data.username}</div>
|
||||
<div>{layoutData.user.username}</div>
|
||||
<div>
|
||||
{data.country ? <Flag countryCode={data.country} tiny /> : null}
|
||||
{data.user.country ? (
|
||||
<Flag countryCode={data.user.country} tiny />
|
||||
) : null}
|
||||
</div>
|
||||
</h2>
|
||||
<TeamInfo />
|
||||
</div>
|
||||
<div className="u__socials">
|
||||
{data.twitch ? (
|
||||
<SocialLink type="twitch" identifier={data.twitch} />
|
||||
{data.user.twitch ? (
|
||||
<SocialLink type="twitch" identifier={data.user.twitch} />
|
||||
) : null}
|
||||
{data.twitter ? (
|
||||
<SocialLink type="twitter" identifier={data.twitter} />
|
||||
{data.user.twitter ? (
|
||||
<SocialLink type="twitter" identifier={data.user.twitter} />
|
||||
) : null}
|
||||
{data.youtubeId ? (
|
||||
<SocialLink type="youtube" identifier={data.youtubeId} />
|
||||
{data.user.youtubeId ? (
|
||||
<SocialLink type="youtube" identifier={data.user.youtubeId} />
|
||||
) : null}
|
||||
{data.battlefy ? (
|
||||
<SocialLink type="battlefy" identifier={data.battlefy} />
|
||||
{data.user.battlefy ? (
|
||||
<SocialLink type="battlefy" identifier={data.user.battlefy} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<BannedInfo />
|
||||
<ExtraInfos />
|
||||
<WeaponPool />
|
||||
<TopPlacements />
|
||||
<BadgeDisplay badges={data.badges} key={data.id} />
|
||||
{data.bio && <article>{data.bio}</article>}
|
||||
<BadgeDisplay badges={data.user.badges} key={layoutData.user.id} />
|
||||
{data.user.bio && <article>{data.user.bio}</article>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamInfo() {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const { team } = parentRoute.data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
if (!team) return null;
|
||||
if (!data.user.team) return null;
|
||||
|
||||
return (
|
||||
<Link to={teamPage(team.customUrl)} className="u__team">
|
||||
{team.avatarUrl ? (
|
||||
<Link to={teamPage(data.user.team.customUrl)} className="u__team">
|
||||
{data.user.team.avatarUrl ? (
|
||||
<img
|
||||
alt=""
|
||||
src={userSubmittedImage(team.avatarUrl)}
|
||||
src={userSubmittedImage(data.user.team.avatarUrl)}
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : null}
|
||||
{team.name}
|
||||
{data.user.team.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -152,55 +158,53 @@ function SocialLinkIcon({ type }: Pick<SocialLinkProps, "type">) {
|
||||
|
||||
function ExtraInfos() {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const motionSensText =
|
||||
typeof data.motionSens === "number"
|
||||
? `${t("user:motion")} ${rawSensToString(data.motionSens)}`
|
||||
typeof data.user.motionSens === "number"
|
||||
? `${t("user:motion")} ${rawSensToString(data.user.motionSens)}`
|
||||
: null;
|
||||
|
||||
const stickSensText =
|
||||
typeof data.stickSens === "number"
|
||||
? `${t("user:stick")} ${rawSensToString(data.stickSens)}`
|
||||
typeof data.user.stickSens === "number"
|
||||
? `${t("user:stick")} ${rawSensToString(data.user.stickSens)}`
|
||||
: null;
|
||||
|
||||
if (
|
||||
!data.inGameName &&
|
||||
typeof data.stickSens !== "number" &&
|
||||
!data.discordUniqueName &&
|
||||
!data.plusTier
|
||||
!data.user.inGameName &&
|
||||
typeof data.user.stickSens !== "number" &&
|
||||
!data.user.discordUniqueName &&
|
||||
!data.user.plusTier
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="u__extra-infos">
|
||||
{data.discordUniqueName && (
|
||||
{data.user.discordUniqueName && (
|
||||
<div className="u__extra-info">
|
||||
<span className="u__extra-info__heading">
|
||||
<DiscordIcon />
|
||||
</span>{" "}
|
||||
{data.discordUniqueName}
|
||||
{data.user.discordUniqueName}
|
||||
</div>
|
||||
)}
|
||||
{data.inGameName && (
|
||||
{data.user.inGameName && (
|
||||
<div className="u__extra-info">
|
||||
<span className="u__extra-info__heading">{t("user:ign.short")}</span>{" "}
|
||||
{data.inGameName}
|
||||
{data.user.inGameName}
|
||||
</div>
|
||||
)}
|
||||
{typeof data.stickSens === "number" && (
|
||||
{typeof data.user.stickSens === "number" && (
|
||||
<div className="u__extra-info">
|
||||
<span className="u__extra-info__heading">{t("user:sens")}</span>{" "}
|
||||
{[motionSensText, stickSensText].filter(Boolean).join(" / ")}
|
||||
</div>
|
||||
)}
|
||||
{data.plusTier && (
|
||||
{data.user.plusTier && (
|
||||
<div className="u__extra-info">
|
||||
<Image path={navIconUrl("plus")} width={20} height={20} alt="" />{" "}
|
||||
{data.plusTier}
|
||||
{data.user.plusTier}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -208,15 +212,13 @@ function ExtraInfos() {
|
||||
}
|
||||
|
||||
function WeaponPool() {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
if (data.weapons.length === 0) return null;
|
||||
if (data.user.weapons.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="stack horizontal sm justify-center">
|
||||
{data.weapons.map((weapon, i) => {
|
||||
{data.user.weapons.map((weapon, i) => {
|
||||
return (
|
||||
<div key={weapon.weaponSplId} className="u__weapon">
|
||||
<WeaponImage
|
||||
@@ -234,20 +236,20 @@ function WeaponPool() {
|
||||
}
|
||||
|
||||
function TopPlacements() {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
if (!data.playerId) return null;
|
||||
if (data.user.topPlacements.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={topSearchPlayerPage(data.playerId)}
|
||||
to={topSearchPlayerPage(data.user.topPlacements[0].playerId)}
|
||||
className="u__placements"
|
||||
data-testid="placements-box"
|
||||
>
|
||||
{modesShort.map((mode) => {
|
||||
const placement = data.topPlacements[mode];
|
||||
const placement = data.user.topPlacements.find(
|
||||
(placement) => placement.mode === mode,
|
||||
);
|
||||
|
||||
if (!placement) return null;
|
||||
|
||||
@@ -263,3 +265,40 @@ function TopPlacements() {
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function BannedInfo() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const { banned, bannedReason } = data.banned ?? {};
|
||||
|
||||
if (!banned) return null;
|
||||
|
||||
const ends = (() => {
|
||||
if (!banned || banned === 1) return null;
|
||||
|
||||
return databaseTimestampToDate(banned);
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h2 className="text-warning">Account suspended</h2>
|
||||
{bannedReason ? <div>Reason: {bannedReason}</div> : null}
|
||||
{ends ? (
|
||||
<div suppressHydrationWarning>
|
||||
Ends:{" "}
|
||||
{ends.toLocaleString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Ends: <i>no end time set</i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ActionFunction, redirect } from "@remix-run/node";
|
||||
import { Form, useMatches } from "@remix-run/react";
|
||||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { FormErrors } from "~/components/FormErrors";
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
HIGHLIGHT_TOURNAMENT_CHECKBOX_NAME,
|
||||
UserResultsTable,
|
||||
} from "~/features/user-page/components/UserResultsTable";
|
||||
import type { UserPageLoaderData } from "~/features/user-page/routes/u.$identifier";
|
||||
import { normalizeFormFieldArray } from "~/utils/arrays";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { parseRequestPayload } from "~/utils/remix";
|
||||
import { userResultsPage } from "~/utils/urls";
|
||||
|
||||
import { loader } from "../loaders/u.$identifier.results.server";
|
||||
export { loader };
|
||||
|
||||
const editHighlightsActionSchema = z.object({
|
||||
[HIGHLIGHT_CHECKBOX_NAME]: z.optional(
|
||||
z.union([z.array(z.string()), z.string()]),
|
||||
@@ -51,10 +52,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
|
||||
export default function ResultHighlightsEditPage() {
|
||||
const { t } = useTranslation(["common", "user"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
|
||||
invariant(parentRoute);
|
||||
const userPageData = parentRoute.data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<Form method="post" className="stack md items-start">
|
||||
@@ -64,7 +62,7 @@ export default function ResultHighlightsEditPage() {
|
||||
<legend>{t("user:results.highlights.explanation")}</legend>
|
||||
<UserResultsTable
|
||||
id="user-results-highlight-selection"
|
||||
results={userPageData.results}
|
||||
results={data.results}
|
||||
hasHighlightCheckboxes
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { useLoaderData, useMatches } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, LinkButton } from "~/components/Button";
|
||||
import { Section } from "~/components/Section";
|
||||
@@ -9,14 +9,19 @@ import invariant from "~/utils/invariant";
|
||||
import { userResultsEditHighlightsPage } from "~/utils/urls";
|
||||
import type { UserPageLoaderData } from "../../../features/user-page/routes/u.$identifier";
|
||||
|
||||
import { loader } from "../loaders/u.$identifier.results.server";
|
||||
export { loader };
|
||||
|
||||
export default function UserResultsPage() {
|
||||
const user = useUser();
|
||||
const { t } = useTranslation("user");
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const userPageData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
const highlightedResults = userPageData.results.filter(
|
||||
const highlightedResults = data.results.filter(
|
||||
(result) => result.isHighlight,
|
||||
);
|
||||
const hasHighlightedResults = highlightedResults.length > 0;
|
||||
@@ -27,11 +32,11 @@ export default function UserResultsPage() {
|
||||
revive: (v) => (!hasHighlightedResults ? true : v === "true"),
|
||||
});
|
||||
|
||||
const resultsToShow = showAll ? userPageData.results : highlightedResults;
|
||||
const resultsToShow = showAll ? data.results : highlightedResults;
|
||||
|
||||
return (
|
||||
<div className="stack lg">
|
||||
{user?.id === userPageData.id ? (
|
||||
{user?.id === layoutData.user.id ? (
|
||||
<LinkButton
|
||||
to={userResultsEditHighlightsPage(user)}
|
||||
className="ml-auto"
|
||||
|
||||
@@ -72,11 +72,9 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
);
|
||||
const {
|
||||
info = "weapons",
|
||||
page,
|
||||
page = 1,
|
||||
season = currentOrPreviousSeason(new Date())!.nth,
|
||||
} = parsedSearchParams.success
|
||||
? parsedSearchParams.data
|
||||
: seasonsSearchParamsSchema.parse({});
|
||||
} = parsedSearchParams.success ? parsedSearchParams.data : {};
|
||||
|
||||
const user = notFoundIfFalsy(
|
||||
await UserRepository.identifierToUserId(identifier),
|
||||
@@ -292,7 +290,7 @@ function Rank({ currentOrdinal }: { currentOrdinal: number }) {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const parentRouteData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
const maxOrdinal = Math.max(...data.skills.map((s) => s.ordinal));
|
||||
|
||||
@@ -300,7 +298,7 @@ function Rank({ currentOrdinal }: { currentOrdinal: number }) {
|
||||
|
||||
const topTenPlacement = playerTopTenPlacement({
|
||||
season: data.season,
|
||||
userId: parentRouteData.id,
|
||||
userId: layoutData.user.id,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -422,7 +420,7 @@ function Stages({
|
||||
}) {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["user", "game-misc"]);
|
||||
const parentPageData = atOrError(useMatches(), -2).data as UserPageLoaderData;
|
||||
const layoutData = atOrError(useMatches(), -2).data as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<div className="stack horizontal justify-center md flex-wrap">
|
||||
@@ -461,7 +459,7 @@ function Stages({
|
||||
modeShort={mode}
|
||||
season={data.season}
|
||||
stageId={id}
|
||||
userId={parentPageData.id}
|
||||
userId={layoutData.user.id}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
@@ -704,8 +702,8 @@ function Match({
|
||||
const { t } = useTranslation(["user"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const userPageData = parentRoute.data as UserPageLoaderData;
|
||||
const userId = userPageData.id;
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const userId = layoutData.user.id;
|
||||
|
||||
const score = match.winnerGroupIds.reduce(
|
||||
(acc, cur) => [
|
||||
|
||||
@@ -3,28 +3,17 @@ import type {
|
||||
MetaFunction,
|
||||
SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import { Outlet, useLoaderData, useLocation } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { Outlet, useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubNav, SubNavLink } from "~/components/SubNav";
|
||||
import { countArtByUserId } from "~/features/art/queries/countArtByUserId.server";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { getUserId } from "~/features/auth/core/user.server";
|
||||
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
|
||||
import { userIsBanned } from "~/features/ban/core/banned.server";
|
||||
import * as BuildRepository from "~/features/builds/BuildRepository.server";
|
||||
import { userTopPlacements } from "~/features/top-search";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { findVods } from "~/features/vods/queries/findVods.server";
|
||||
import { canAddCustomizedColorsToUserProfile, isAdmin } from "~/permissions";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { type SendouRouteHandle, notFoundIfFalsy } from "~/utils/remix";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
USER_SEARCH_PAGE,
|
||||
isCustomUrl,
|
||||
navIconUrl,
|
||||
userArtPage,
|
||||
userBuildsPage,
|
||||
@@ -34,14 +23,13 @@ import {
|
||||
userSeasonsPage,
|
||||
userVodsPage,
|
||||
} from "~/utils/urls";
|
||||
import { userParamsSchema } from "../user-page-schemas.server";
|
||||
|
||||
import "~/styles/u.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: makeTitle(data.username) }];
|
||||
return [{ title: makeTitle(data.user.username) }];
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
@@ -58,8 +46,8 @@ export const handle: SendouRouteHandle = {
|
||||
type: "IMAGE",
|
||||
},
|
||||
{
|
||||
text: data.username,
|
||||
href: userPage(data),
|
||||
text: data.user.username,
|
||||
href: userPage(data.user),
|
||||
type: "TEXT",
|
||||
},
|
||||
];
|
||||
@@ -70,33 +58,20 @@ export type UserPageLoaderData = SerializeFrom<typeof loader>;
|
||||
|
||||
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
const loggedInUser = await getUserId(request);
|
||||
const { identifier } = userParamsSchema.parse(params);
|
||||
|
||||
const user = notFoundIfFalsy(
|
||||
await UserRepository.findByIdentifier(identifier),
|
||||
await UserRepository.findLayoutDataByIdentifier(
|
||||
params.identifier!,
|
||||
loggedInUser?.id,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
...user,
|
||||
...userTopPlacements(user.id),
|
||||
discordUniqueName: user.showDiscordUniqueName
|
||||
? user.discordUniqueName
|
||||
: null,
|
||||
banned:
|
||||
isAdmin(loggedInUser) && userIsBanned(user.id)
|
||||
? { banned: user.banned, bannedReason: user.bannedReason }
|
||||
: undefined,
|
||||
css: canAddCustomizedColorsToUserProfile(user) ? user.css : undefined,
|
||||
badges: await BadgeRepository.findByOwnerId({
|
||||
userId: user.id,
|
||||
favoriteBadgeId: user.favoriteBadgeId,
|
||||
}),
|
||||
results: await UserRepository.findResultsByUserId(user.id),
|
||||
buildsCount: await BuildRepository.countByUserId({
|
||||
userId: user.id,
|
||||
showPrivate: user.id === loggedInUser?.id,
|
||||
}),
|
||||
vods: findVods({ userId: user.id }),
|
||||
artCount: countArtByUserId(user.id),
|
||||
user: {
|
||||
...user,
|
||||
css: undefined,
|
||||
},
|
||||
css: user.css,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -105,115 +80,51 @@ export default function UserPageLayout() {
|
||||
const user = useUser();
|
||||
const { t } = useTranslation(["common", "user"]);
|
||||
|
||||
const isOwnPage = data.id === user?.id;
|
||||
const isOwnPage = data.user.id === user?.id;
|
||||
|
||||
useReplaceWithCustomUrl();
|
||||
const allResultsCount =
|
||||
data.user.calendarEventResultsCount + data.user.tournamentResultsCount;
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<SubNav>
|
||||
<SubNavLink to={userPage(data)}>
|
||||
<SubNavLink to={userPage(data.user)}>
|
||||
{t("common:header.profile")}
|
||||
</SubNavLink>
|
||||
<SubNavLink to={userSeasonsPage({ user: data })}>
|
||||
<SubNavLink to={userSeasonsPage({ user: data.user })}>
|
||||
{t("user:seasons")}
|
||||
</SubNavLink>
|
||||
{isOwnPage && (
|
||||
<SubNavLink to={userEditProfilePage(data)} prefetch="intent">
|
||||
<SubNavLink to={userEditProfilePage(data.user)} prefetch="intent">
|
||||
{t("common:actions.edit")}
|
||||
</SubNavLink>
|
||||
)}
|
||||
{data.results.length > 0 && (
|
||||
<SubNavLink to={userResultsPage(data)}>
|
||||
{t("common:results")} ({data.results.length})
|
||||
{allResultsCount > 0 && (
|
||||
<SubNavLink to={userResultsPage(data.user)}>
|
||||
{t("common:results")} ({allResultsCount})
|
||||
</SubNavLink>
|
||||
)}
|
||||
{(isOwnPage || data.buildsCount > 0) && (
|
||||
{(isOwnPage || data.user.buildsCount > 0) && (
|
||||
<SubNavLink
|
||||
to={userBuildsPage(data)}
|
||||
to={userBuildsPage(data.user)}
|
||||
prefetch="intent"
|
||||
data-testid="builds-tab"
|
||||
>
|
||||
{t("common:pages.builds")} ({data.buildsCount})
|
||||
{t("common:pages.builds")} ({data.user.buildsCount})
|
||||
</SubNavLink>
|
||||
)}
|
||||
{(isOwnPage || data.vods.length > 0) && (
|
||||
<SubNavLink to={userVodsPage(data)}>
|
||||
{t("common:pages.vods")} ({data.vods.length})
|
||||
{(isOwnPage || data.user.vodsCount > 0) && (
|
||||
<SubNavLink to={userVodsPage(data.user)}>
|
||||
{t("common:pages.vods")} ({data.user.vodsCount})
|
||||
</SubNavLink>
|
||||
)}
|
||||
{(isOwnPage || data.artCount > 0) && (
|
||||
<SubNavLink to={userArtPage(data)} end={false}>
|
||||
{t("common:pages.art")} ({data.artCount})
|
||||
{(isOwnPage || data.user.artCount > 0) && (
|
||||
<SubNavLink to={userArtPage(data.user)} end={false}>
|
||||
{t("common:pages.art")} ({data.user.artCount})
|
||||
</SubNavLink>
|
||||
)}
|
||||
</SubNav>
|
||||
<BannedInfo />
|
||||
<Outlet />
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function useReplaceWithCustomUrl() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const location = useLocation();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!data.customUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const identifier = location.pathname.replace("/u/", "").split("/")[0];
|
||||
invariant(identifier);
|
||||
|
||||
if (isCustomUrl(identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
location.pathname
|
||||
.split("/")
|
||||
.map((part) => (part === identifier ? data.customUrl : part))
|
||||
.join("/"),
|
||||
);
|
||||
}, [location, data.customUrl]);
|
||||
}
|
||||
|
||||
function BannedInfo() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const { banned, bannedReason } = data.banned ?? {};
|
||||
|
||||
if (!banned) return null;
|
||||
|
||||
const ends = (() => {
|
||||
if (!banned || banned === 1) return null;
|
||||
|
||||
return databaseTimestampToDate(banned);
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h2 className="text-warning">Account suspended</h2>
|
||||
{bannedReason ? <div>Reason: {bannedReason}</div> : null}
|
||||
{ends ? (
|
||||
<div suppressHydrationWarning>
|
||||
Ends:{" "}
|
||||
{ends.toLocaleString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Ends: <i>no end time set</i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { useLoaderData, useMatches } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { Popover } from "~/components/Popover";
|
||||
@@ -9,6 +9,9 @@ import type { SendouRouteHandle } from "~/utils/remix";
|
||||
import { newVodPage } from "~/utils/urls";
|
||||
import type { UserPageLoaderData } from "./u.$identifier";
|
||||
|
||||
import { loader } from "../loaders/u.$identifier.vods.server";
|
||||
export { loader };
|
||||
|
||||
import "~/features/vods/vods.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
@@ -19,16 +22,17 @@ export default function UserVodsPage() {
|
||||
const user = useUser();
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const userPageData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="vods__listing__list">
|
||||
{userPageData.id === user?.id ? (
|
||||
{layoutData.user.id === user?.id ? (
|
||||
<div className="stack items-end w-full">
|
||||
<AddVodButton isVideoAdder={user.isVideoAdder} />
|
||||
</div>
|
||||
) : null}
|
||||
{userPageData.vods.map((vod) => (
|
||||
{data.vods.map((vod) => (
|
||||
<VodListing key={vod.id} vod={vod} showUser={false} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { allSeasons } from "../mmr/season";
|
||||
export const userParamsSchema = z.object({ identifier: z.string() });
|
||||
|
||||
export const seasonsSearchParamsSchema = z.object({
|
||||
page: z.coerce.number().default(1),
|
||||
page: z.coerce.number().optional(),
|
||||
info: z.enum(["weapons", "stages", "mates", "enemies"]).optional(),
|
||||
season: z.coerce
|
||||
.number()
|
||||
|
||||
@@ -40,11 +40,11 @@ export const createVod = sql.transaction(
|
||||
},
|
||||
) => {
|
||||
const video = createVideoStm.get({
|
||||
id: args.id,
|
||||
id: args.id ?? null,
|
||||
title: args.title,
|
||||
type: args.type,
|
||||
youtubeDate: args.youtubeDate,
|
||||
eventId: args.eventId,
|
||||
eventId: args.eventId ?? null,
|
||||
youtubeId: args.youtubeId,
|
||||
submitterUserId: args.submitterUserId,
|
||||
validatedAt: args.isValidated
|
||||
@@ -63,8 +63,8 @@ export const createVod = sql.transaction(
|
||||
for (const [i, weaponSplId] of match.weapons.entries()) {
|
||||
createVideoMatchPlayerStm.run({
|
||||
videoMatchId: videoMatch.id,
|
||||
playerUserId: args.povUserId,
|
||||
playerName: args.povUserName,
|
||||
playerUserId: args.povUserId ?? null,
|
||||
playerName: args.povUserName ?? null,
|
||||
weaponSplId,
|
||||
player: i + 1,
|
||||
});
|
||||
|
||||
@@ -63,11 +63,11 @@ export function findVods({
|
||||
const stmToUse = userId ? stmByUser : stm;
|
||||
|
||||
const vods = stmToUse.all({
|
||||
weapon,
|
||||
mode,
|
||||
stageId,
|
||||
type,
|
||||
userId,
|
||||
weapon: weapon ?? null,
|
||||
mode: mode ?? null,
|
||||
stageId: stageId ?? null,
|
||||
type: type ?? null,
|
||||
userId: userId ?? null,
|
||||
}) as any[];
|
||||
|
||||
return vods
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useTimeoutState = <T>(
|
||||
] => {
|
||||
const [state, _setState] = React.useState<T>(defaultState);
|
||||
const [currentTimeoutId, setCurrentTimeoutId] = React.useState<
|
||||
NodeJS.Timeout | undefined
|
||||
Timer | undefined
|
||||
>();
|
||||
|
||||
const setState = React.useCallback(
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const ExtraFields = suite("Update results with extra fields");
|
||||
|
||||
ExtraFields.before.each(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
ExtraFields("Extra fields when updating a match", () => {
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
// @ts-expect-error incomplete types
|
||||
weather: "rainy", // Extra field.
|
||||
opponent1: {
|
||||
score: 3,
|
||||
result: "win",
|
||||
},
|
||||
opponent2: {
|
||||
score: 1,
|
||||
result: "loss",
|
||||
},
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: {
|
||||
score: 3,
|
||||
result: "win",
|
||||
// @ts-expect-error incomplete types
|
||||
foo: 42, // Extra field.
|
||||
},
|
||||
opponent2: {
|
||||
score: 1,
|
||||
result: "loss",
|
||||
},
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 2,
|
||||
opponent1: {
|
||||
score: 3,
|
||||
result: "win",
|
||||
},
|
||||
opponent2: {
|
||||
score: 1,
|
||||
result: "loss",
|
||||
// @ts-expect-error incomplete types
|
||||
info: { replacements: [1, 2] }, // Extra field.
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 0).weather, "rainy");
|
||||
assert.equal(storage.select<any>("match", 1).opponent1.foo, 42);
|
||||
assert.equal(storage.select<any>("match", 2).opponent2.info, {
|
||||
replacements: [1, 2],
|
||||
});
|
||||
});
|
||||
|
||||
ExtraFields.run();
|
||||
@@ -1,98 +1,95 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const DeleteStage = suite("Delete stage");
|
||||
describe("Delete stage", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
DeleteStage.before.each(() => {
|
||||
storage.reset();
|
||||
test("should delete a stage and all its linked data", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.delete.stage(0);
|
||||
|
||||
const stages = storage.select("stage")!;
|
||||
const groups = storage.select("group")!;
|
||||
const rounds = storage.select("round")!;
|
||||
const matches = storage.select<any>("match")!;
|
||||
|
||||
expect(stages.length).toBe(0);
|
||||
expect(groups.length).toBe(0);
|
||||
expect(rounds.length).toBe(0);
|
||||
expect(matches.length).toBe(0);
|
||||
});
|
||||
|
||||
test("should delete one stage and only its linked data", () => {
|
||||
manager.create({
|
||||
name: "Example 1",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example 2",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.delete.stage(0);
|
||||
|
||||
const stages = storage.select<any>("stage")!;
|
||||
const groups = storage.select<any>("group")!;
|
||||
const rounds = storage.select<any>("round")!;
|
||||
const matches = storage.select<any>("match")!;
|
||||
|
||||
expect(stages.length).toBe(1);
|
||||
expect(groups.length).toBe(1);
|
||||
expect(rounds.length).toBe(2);
|
||||
expect(matches.length).toBe(3);
|
||||
|
||||
// Remaining data
|
||||
expect(stages[0].id).toBe(1);
|
||||
expect(groups[0].id).toBe(1);
|
||||
expect(rounds[0].id).toBe(2);
|
||||
expect(matches[0].id).toBe(3);
|
||||
});
|
||||
|
||||
test("should delete all stages of the tournament", () => {
|
||||
manager.create({
|
||||
name: "Example 1",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example 2",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.delete.tournament(0);
|
||||
|
||||
const stages = storage.select("stage")!;
|
||||
const groups = storage.select("group")!;
|
||||
const rounds = storage.select("round")!;
|
||||
const matches = storage.select<any>("match")!;
|
||||
|
||||
expect(stages.length).toBe(0);
|
||||
expect(groups.length).toBe(0);
|
||||
expect(rounds.length).toBe(0);
|
||||
expect(matches.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
DeleteStage("should delete a stage and all its linked data", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.delete.stage(0);
|
||||
|
||||
const stages = storage.select("stage")!;
|
||||
const groups = storage.select("group")!;
|
||||
const rounds = storage.select("round")!;
|
||||
const matches = storage.select<any>("match")!;
|
||||
|
||||
assert.equal(stages.length, 0);
|
||||
assert.equal(groups.length, 0);
|
||||
assert.equal(rounds.length, 0);
|
||||
assert.equal(matches.length, 0);
|
||||
});
|
||||
|
||||
DeleteStage("should delete one stage and only its linked data", () => {
|
||||
manager.create({
|
||||
name: "Example 1",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example 2",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.delete.stage(0);
|
||||
|
||||
const stages = storage.select<any>("stage")!;
|
||||
const groups = storage.select<any>("group")!;
|
||||
const rounds = storage.select<any>("round")!;
|
||||
const matches = storage.select<any>("match")!;
|
||||
|
||||
assert.equal(stages.length, 1);
|
||||
assert.equal(groups.length, 1);
|
||||
assert.equal(rounds.length, 2);
|
||||
assert.equal(matches.length, 3);
|
||||
|
||||
// Remaining data
|
||||
assert.equal(stages[0].id, 1);
|
||||
assert.equal(groups[0].id, 1);
|
||||
assert.equal(rounds[0].id, 2);
|
||||
assert.equal(matches[0].id, 3);
|
||||
});
|
||||
|
||||
DeleteStage("should delete all stages of the tournament", () => {
|
||||
manager.create({
|
||||
name: "Example 1",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example 2",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
manager.delete.tournament(0);
|
||||
|
||||
const stages = storage.select("stage")!;
|
||||
const groups = storage.select("group")!;
|
||||
const rounds = storage.select("round")!;
|
||||
const matches = storage.select<any>("match")!;
|
||||
|
||||
assert.equal(stages.length, 0);
|
||||
assert.equal(groups.length, 0);
|
||||
assert.equal(rounds.length, 0);
|
||||
assert.equal(matches.length, 0);
|
||||
});
|
||||
|
||||
DeleteStage.run();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { Status } from "~/db/types";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
@@ -7,45 +6,39 @@ import { BracketsManager } from "../manager";
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const CreateDoubleElimination = suite("Delete stage");
|
||||
|
||||
CreateDoubleElimination.before.each(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
CreateDoubleElimination("should create a double elimination stage", () => {
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
|
||||
describe("Delete stage", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
const stage = storage.select<any>("stage", 0);
|
||||
assert.equal(stage.name, "Amateur");
|
||||
assert.equal(stage.type, "double_elimination");
|
||||
test("should create a double elimination stage", () => {
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("group")!.length, 3);
|
||||
assert.equal(storage.select<any>("round")!.length, 4 + 6 + 1);
|
||||
assert.equal(storage.select<any>("match")!.length, 30);
|
||||
});
|
||||
const stage = storage.select<any>("stage", 0);
|
||||
expect(stage.name).toBe("Amateur");
|
||||
expect(stage.type).toBe("double_elimination");
|
||||
|
||||
CreateDoubleElimination(
|
||||
"should create a tournament with 256+ tournaments",
|
||||
() => {
|
||||
expect(storage.select<any>("group")!.length).toBe(3);
|
||||
expect(storage.select<any>("round")!.length).toBe(4 + 6 + 1);
|
||||
expect(storage.select<any>("match")!.length).toBe(30);
|
||||
});
|
||||
|
||||
test("should create a tournament with 256+ tournaments", () => {
|
||||
manager.create({
|
||||
name: "Example with 256 participants",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
settings: { size: 256 },
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
CreateDoubleElimination(
|
||||
"should create a tournament with a double grand final",
|
||||
() => {
|
||||
test("should create a tournament with a double grand final", () => {
|
||||
manager.create({
|
||||
name: "Example with double grand final",
|
||||
tournamentId: 0,
|
||||
@@ -54,23 +47,18 @@ CreateDoubleElimination(
|
||||
settings: { grandFinal: "double", seedOrdering: ["natural"] },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("group")!.length, 3);
|
||||
assert.equal(storage.select<any>("round")!.length, 3 + 4 + 2);
|
||||
assert.equal(storage.select<any>("match")!.length, 15);
|
||||
},
|
||||
);
|
||||
|
||||
const MatchUpdateDoubleElimination = suite(
|
||||
"Previous and next match update in double elimination stage",
|
||||
);
|
||||
|
||||
MatchUpdateDoubleElimination.before.each(() => {
|
||||
storage.reset();
|
||||
expect(storage.select<any>("group")!.length).toBe(3);
|
||||
expect(storage.select<any>("round")!.length).toBe(3 + 4 + 2);
|
||||
expect(storage.select<any>("match")!.length).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
MatchUpdateDoubleElimination(
|
||||
"should end a match and determine next matches",
|
||||
() => {
|
||||
describe("Previous and next match update in double elimination stage", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
test("should end a match and determine next matches", () => {
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
@@ -80,7 +68,7 @@ MatchUpdateDoubleElimination(
|
||||
});
|
||||
|
||||
const before = storage.select<any>("match", 8); // First match of WB round 2
|
||||
assert.equal(before.opponent2.id, null);
|
||||
expect(before.opponent2.id).toBeNull();
|
||||
|
||||
manager.update.match({
|
||||
id: 0, // First match of WB round 1
|
||||
@@ -100,31 +88,24 @@ MatchUpdateDoubleElimination(
|
||||
opponent2: { score: 10 },
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 8).opponent1.id, // Determined opponent for WB round 2
|
||||
storage.select<any>("match", 0).opponent1.id, // Winner of first match round 1
|
||||
);
|
||||
).toBe(storage.select<any>("match", 0).opponent1.id); // Winner of first match round 1
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 8).opponent2.id, // Determined opponent for WB round 2
|
||||
storage.select<any>("match", 1).opponent2.id, // Winner of second match round 1
|
||||
);
|
||||
).toBe(storage.select<any>("match", 1).opponent2.id); // Winner of second match round 1
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 15).opponent2.id, // Determined opponent for LB round 1
|
||||
storage.select<any>("match", 1).opponent1.id, // Loser of second match round 1
|
||||
);
|
||||
).toBe(storage.select<any>("match", 1).opponent1.id); // Loser of second match round 1
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 19).opponent2.id, // Determined opponent for LB round 2
|
||||
storage.select<any>("match", 0).opponent2.id, // Loser of first match round 1
|
||||
);
|
||||
},
|
||||
);
|
||||
).toBe(storage.select<any>("match", 0).opponent2.id); // Loser of first match round 1
|
||||
});
|
||||
|
||||
MatchUpdateDoubleElimination(
|
||||
"should propagate winner when BYE is already in next match in loser bracket",
|
||||
() => {
|
||||
test("should propagate winner when BYE is already in next match in loser bracket", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -142,99 +123,92 @@ MatchUpdateDoubleElimination(
|
||||
const loserId = storage.select<any>("match", 1).opponent2.id;
|
||||
let matchSemiLB = storage.select<any>("match", 3);
|
||||
|
||||
assert.equal(matchSemiLB.opponent2.id, loserId);
|
||||
assert.equal(matchSemiLB.opponent2.result, "win");
|
||||
assert.equal(matchSemiLB.status, Status.Completed);
|
||||
expect(matchSemiLB.opponent2.id).toBe(loserId);
|
||||
expect(matchSemiLB.opponent2.result).toBe("win");
|
||||
expect(matchSemiLB.status).toBe(Status.Completed);
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 4).opponent2.id, // Propagated winner in LB Final because of the BYE.
|
||||
loserId,
|
||||
);
|
||||
).toBe(loserId);
|
||||
|
||||
manager.reset.matchResults(1); // Second match of WB round 1
|
||||
|
||||
matchSemiLB = storage.select<any>("match", 3);
|
||||
assert.equal(matchSemiLB.opponent2.id, null);
|
||||
assert.equal(matchSemiLB.opponent2.result, undefined);
|
||||
assert.equal(matchSemiLB.status, Status.Locked);
|
||||
expect(matchSemiLB.opponent2.id).toBeNull();
|
||||
expect(matchSemiLB.opponent2.result).toBeUndefined();
|
||||
expect(matchSemiLB.status).toBe(Status.Locked);
|
||||
|
||||
assert.equal(storage.select<any>("match", 4).opponent2.id, null); // Propagated winner is removed.
|
||||
},
|
||||
);
|
||||
|
||||
MatchUpdateDoubleElimination("should determine matches in grand final", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { grandFinal: "double" },
|
||||
expect(storage.select<any>("match", 4).opponent2.id).toBeNull(); // Propagated winner is removed.
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 0, // First match of WB round 1
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
test("should determine matches in grand final", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { grandFinal: "double" },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 0, // First match of WB round 1
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1, // Second match of WB round 1
|
||||
opponent1: { score: 13 },
|
||||
opponent2: { score: 16, result: "win" },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 2, // WB Final
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 9 },
|
||||
});
|
||||
|
||||
expect(
|
||||
storage.select<any>("match", 5).opponent1.id, // Determined opponent for the grand final (round 1)
|
||||
).toBe(storage.select<any>("match", 0).opponent1.id); // Winner of WB Final
|
||||
|
||||
manager.update.match({
|
||||
id: 3, // Only match of LB round 1
|
||||
opponent1: { score: 12, result: "win" }, // Team 4
|
||||
opponent2: { score: 8 },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 4, // LB Final
|
||||
opponent1: { score: 14, result: "win" }, // Team 3
|
||||
opponent2: { score: 7 },
|
||||
});
|
||||
|
||||
expect(
|
||||
storage.select<any>("match", 5).opponent2.id, // Determined opponent for the grand final (round 1)
|
||||
).toBe(storage.select<any>("match", 1).opponent2.id); // Winner of LB Final
|
||||
|
||||
manager.update.match({
|
||||
id: 5, // Grand Final round 1
|
||||
opponent1: { score: 10 },
|
||||
opponent2: { score: 16, result: "win" }, // Team 3
|
||||
});
|
||||
|
||||
expect(
|
||||
storage.select<any>("match", 6).opponent2.id, // Determined opponent for the grand final (round 2)
|
||||
).toBe(storage.select<any>("match", 1).opponent2.id); // Winner of LB Final
|
||||
|
||||
expect(storage.select<any>("match", 5).status).toBe(Status.Completed); // Grand final (round 1)
|
||||
expect(storage.select<any>("match", 6).status).toBe(Status.Ready); // Grand final (round 2)
|
||||
|
||||
manager.update.match({
|
||||
id: 6, // Grand Final round 2
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 10 },
|
||||
});
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1, // Second match of WB round 1
|
||||
opponent1: { score: 13 },
|
||||
opponent2: { score: 16, result: "win" },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 2, // WB Final
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 9 },
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
storage.select<any>("match", 5).opponent1.id, // Determined opponent for the grand final (round 1)
|
||||
storage.select<any>("match", 0).opponent1.id, // Winner of WB Final
|
||||
);
|
||||
|
||||
manager.update.match({
|
||||
id: 3, // Only match of LB round 1
|
||||
opponent1: { score: 12, result: "win" }, // Team 4
|
||||
opponent2: { score: 8 },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 4, // LB Final
|
||||
opponent1: { score: 14, result: "win" }, // Team 3
|
||||
opponent2: { score: 7 },
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
storage.select<any>("match", 5).opponent2.id, // Determined opponent for the grand final (round 1)
|
||||
storage.select<any>("match", 1).opponent2.id, // Winner of LB Final
|
||||
);
|
||||
|
||||
manager.update.match({
|
||||
id: 5, // Grand Final round 1
|
||||
opponent1: { score: 10 },
|
||||
opponent2: { score: 16, result: "win" }, // Team 3
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
storage.select<any>("match", 6).opponent2.id, // Determined opponent for the grand final (round 2)
|
||||
storage.select<any>("match", 1).opponent2.id, // Winner of LB Final
|
||||
);
|
||||
|
||||
assert.equal(storage.select<any>("match", 5).status, Status.Completed); // Grand final (round 1)
|
||||
assert.equal(storage.select<any>("match", 6).status, Status.Ready); // Grand final (round 2)
|
||||
|
||||
manager.update.match({
|
||||
id: 6, // Grand Final round 2
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 10 },
|
||||
});
|
||||
});
|
||||
|
||||
MatchUpdateDoubleElimination(
|
||||
"should determine next matches and reset them",
|
||||
() => {
|
||||
test("should determine next matches and reset them", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -250,23 +224,19 @@ MatchUpdateDoubleElimination(
|
||||
});
|
||||
|
||||
const beforeReset = storage.select<any>("match", 3); // Determined opponent for LB round 1
|
||||
assert.equal(
|
||||
beforeReset.opponent1.id,
|
||||
expect(beforeReset.opponent1.id).toBe(
|
||||
storage.select<any>("match", 0).opponent2.id,
|
||||
);
|
||||
assert.equal(beforeReset.opponent1.position, 1); // Must be set.
|
||||
expect(beforeReset.opponent1.position).toBe(1); // Must be set.
|
||||
|
||||
manager.reset.matchResults(0); // First match of WB round 1
|
||||
|
||||
const afterReset = storage.select<any>("match", 3); // Determined opponent for LB round 1
|
||||
assert.equal(afterReset.opponent1.id, null);
|
||||
assert.equal(afterReset.opponent1.position, 1); // It must stay.
|
||||
},
|
||||
);
|
||||
expect(afterReset.opponent1.id).toBeNull();
|
||||
expect(afterReset.opponent1.position).toBe(1); // It must stay.
|
||||
});
|
||||
|
||||
MatchUpdateDoubleElimination(
|
||||
"should choose the correct previous and next matches based on losers ordering",
|
||||
() => {
|
||||
test("should choose the correct previous and next matches based on losers ordering", () => {
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
@@ -279,22 +249,19 @@ MatchUpdateDoubleElimination(
|
||||
});
|
||||
|
||||
manager.update.match({ id: 0, opponent1: { result: "win" } }); // WB 1.1
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 18).opponent2.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
|
||||
storage.select<any>("match", 0).opponent2.id, // Loser of first match round 1
|
||||
);
|
||||
).toBe(storage.select<any>("match", 0).opponent2.id); // Loser of first match round 1
|
||||
|
||||
manager.update.match({ id: 1, opponent1: { result: "win" } }); // WB 1.2
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 18).opponent1.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
|
||||
storage.select<any>("match", 1).opponent2.id, // Loser of second match round 1
|
||||
);
|
||||
).toBe(storage.select<any>("match", 1).opponent2.id); // Loser of second match round 1
|
||||
|
||||
manager.update.match({ id: 8, opponent1: { result: "win" } }); // WB 2.1
|
||||
assert.equal(
|
||||
expect(
|
||||
storage.select<any>("match", 22).opponent1.id, // Determined opponent for last match of LB round 2 (reverse ordering for losers)
|
||||
storage.select<any>("match", 8).opponent2.id, // Loser of first match round 2
|
||||
);
|
||||
).toBe(storage.select<any>("match", 8).opponent2.id); // Loser of first match round 2
|
||||
|
||||
manager.update.match({ id: 6, opponent1: { result: "win" } }); // WB 1.7
|
||||
manager.update.match({ id: 7, opponent1: { result: "win" } }); // WB 1.8
|
||||
@@ -302,13 +269,10 @@ MatchUpdateDoubleElimination(
|
||||
manager.update.match({ id: 15, opponent1: { result: "win" } }); // LB 1.1
|
||||
manager.update.match({ id: 19, opponent1: { result: "win" } }); // LB 2.1
|
||||
|
||||
assert.equal(storage.select<any>("match", 8).status, Status.Completed); // WB 2.1
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("match", 8).status).toBe(Status.Completed); // WB 2.1
|
||||
});
|
||||
|
||||
MatchUpdateDoubleElimination(
|
||||
"should send the losers to the right LB matches in round 1",
|
||||
() => {
|
||||
test("should send the losers to the right LB matches in round 1", () => {
|
||||
manager.create({
|
||||
name: "Example with inner_outer loser ordering",
|
||||
tournamentId: 0,
|
||||
@@ -319,10 +283,10 @@ MatchUpdateDoubleElimination(
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 7).opponent1.position, 1);
|
||||
assert.equal(storage.select<any>("match", 7).opponent2.position, 4);
|
||||
assert.equal(storage.select<any>("match", 8).opponent1.position, 2);
|
||||
assert.equal(storage.select<any>("match", 8).opponent2.position, 3);
|
||||
expect(storage.select<any>("match", 7).opponent1.position).toBe(1);
|
||||
expect(storage.select<any>("match", 7).opponent2.position).toBe(4);
|
||||
expect(storage.select<any>("match", 8).opponent1.position).toBe(2);
|
||||
expect(storage.select<any>("match", 8).opponent2.position).toBe(3);
|
||||
|
||||
// Match of position 1.
|
||||
manager.update.match({
|
||||
@@ -330,7 +294,7 @@ MatchUpdateDoubleElimination(
|
||||
opponent1: { result: "win" }, // Loser id: 7.
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 7).opponent1.id, 8);
|
||||
expect(storage.select<any>("match", 7).opponent1.id).toBe(8);
|
||||
|
||||
// Match of position 2.
|
||||
manager.update.match({
|
||||
@@ -338,7 +302,7 @@ MatchUpdateDoubleElimination(
|
||||
opponent1: { result: "win" }, // Loser id: 4.
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 8).opponent1.id, 5);
|
||||
expect(storage.select<any>("match", 8).opponent1.id).toBe(5);
|
||||
|
||||
// Match of position 3.
|
||||
manager.update.match({
|
||||
@@ -346,7 +310,7 @@ MatchUpdateDoubleElimination(
|
||||
opponent1: { result: "win" }, // Loser id: 6.
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 8).opponent2.id, 7);
|
||||
expect(storage.select<any>("match", 8).opponent2.id).toBe(7);
|
||||
|
||||
// Match of position 4.
|
||||
manager.update.match({
|
||||
@@ -354,79 +318,68 @@ MatchUpdateDoubleElimination(
|
||||
opponent1: { result: "win" }, // Loser id: 5.
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 7).opponent2.id, 6);
|
||||
},
|
||||
);
|
||||
|
||||
const SkipFirstRoundDoubleElimination = suite("Skip first round");
|
||||
|
||||
SkipFirstRoundDoubleElimination.before.each(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Example with double grand final",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
skipFirstRound: true,
|
||||
grandFinal: "double",
|
||||
},
|
||||
expect(storage.select<any>("match", 7).opponent2.id).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
SkipFirstRoundDoubleElimination(
|
||||
"should create a double elimination stage with skip first round option",
|
||||
() => {
|
||||
assert.equal(storage.select<any>("group")!.length, 3);
|
||||
assert.equal(storage.select<any>("round")!.length, 3 + 6 + 2); // One round less in WB.
|
||||
assert.equal(
|
||||
storage.select<any>("match")!.length,
|
||||
describe("Skip first round", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Example with double grand final",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
skipFirstRound: true,
|
||||
grandFinal: "double",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should create a double elimination stage with skip first round option", () => {
|
||||
expect(storage.select<any>("group")!.length).toBe(3);
|
||||
expect(storage.select<any>("round")!.length).toBe(3 + 6 + 2); // One round less in WB.
|
||||
expect(storage.select<any>("match")!.length).toBe(
|
||||
4 + 2 + 1 + (4 + 4 + 2 + 2 + 1 + 1) + (1 + 1),
|
||||
);
|
||||
|
||||
assert.equal(storage.select<any>("round", 0).number, 1); // Even though the "real" first round is skipped, the stored first round's number should be 1.
|
||||
expect(storage.select<any>("round", 0).number).toBe(1); // Even though the "real" first round is skipped, the stored first round's number should be 1.
|
||||
|
||||
assert.equal(storage.select<any>("match", 0).opponent1.id, 1); // First match of WB.
|
||||
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB.
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("match", 0).opponent1.id).toBe(1); // First match of WB.
|
||||
expect(storage.select<any>("match", 7).opponent1.id).toBe(2); // First match of LB.
|
||||
});
|
||||
|
||||
SkipFirstRoundDoubleElimination(
|
||||
"should choose the correct previous and next matches",
|
||||
() => {
|
||||
test("should choose the correct previous and next matches", () => {
|
||||
manager.update.match({ id: 0, opponent1: { result: "win" } });
|
||||
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB Round 1 (must stay).
|
||||
assert.equal(storage.select<any>("match", 12).opponent1.id, 3); // First match of LB Round 2 (must be updated).
|
||||
expect(storage.select<any>("match", 7).opponent1.id).toBe(2); // First match of LB Round 1 (must stay).
|
||||
expect(storage.select<any>("match", 12).opponent1.id).toBe(3); // First match of LB Round 2 (must be updated).
|
||||
|
||||
manager.update.match({ id: 1, opponent1: { result: "win" } });
|
||||
assert.equal(storage.select<any>("match", 7).opponent2.id, 4); // First match of LB Round 1 (must stay).
|
||||
assert.equal(storage.select<any>("match", 11).opponent1.id, 7); // Second match of LB Round 2 (must be updated).
|
||||
expect(storage.select<any>("match", 7).opponent2.id).toBe(4); // First match of LB Round 1 (must stay).
|
||||
expect(storage.select<any>("match", 11).opponent1.id).toBe(7); // Second match of LB Round 2 (must be updated).
|
||||
|
||||
manager.update.match({ id: 4, opponent1: { result: "win" } }); // First match of WB Round 2.
|
||||
assert.equal(storage.select<any>("match", 18).opponent1.id, 5); // First match of LB Round 4.
|
||||
expect(storage.select<any>("match", 18).opponent1.id).toBe(5); // First match of LB Round 4.
|
||||
|
||||
manager.update.match({ id: 7, opponent1: { result: "win" } }); // First match of LB Round 1.
|
||||
assert.equal(storage.select<any>("match", 11).opponent2.id, 2); // First match of LB Round 2.
|
||||
expect(storage.select<any>("match", 11).opponent2.id).toBe(2); // First match of LB Round 2.
|
||||
|
||||
for (let i = 2; i < 21; i++)
|
||||
manager.update.match({ id: i, opponent1: { result: "win" } });
|
||||
|
||||
assert.equal(storage.select<any>("match", 15).opponent1.id, 7); // First match of LB Round 3.
|
||||
expect(storage.select<any>("match", 15).opponent1.id).toBe(7); // First match of LB Round 3.
|
||||
|
||||
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 1.
|
||||
assert.equal(storage.select<any>("match", 21).opponent2.id, 9); // GF Round 1.
|
||||
expect(storage.select<any>("match", 21).opponent1.id).toBe(1); // GF Round 1.
|
||||
expect(storage.select<any>("match", 21).opponent2.id).toBe(9); // GF Round 1.
|
||||
|
||||
manager.update.match({ id: 21, opponent2: { result: "win" } });
|
||||
|
||||
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 2.
|
||||
assert.equal(storage.select<any>("match", 22).opponent2.id, 9); // GF Round 2.
|
||||
expect(storage.select<any>("match", 21).opponent1.id).toBe(1); // GF Round 2.
|
||||
expect(storage.select<any>("match", 22).opponent2.id).toBe(9); // GF Round 2.
|
||||
|
||||
manager.update.match({ id: 22, opponent2: { result: "win" } });
|
||||
},
|
||||
);
|
||||
|
||||
CreateDoubleElimination.run();
|
||||
MatchUpdateDoubleElimination.run();
|
||||
SkipFirstRoundDoubleElimination.run();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,69 +1,63 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const FindSingleElimination = suite(
|
||||
"Find previous and next matches in single elimination",
|
||||
);
|
||||
|
||||
FindSingleElimination.before.each(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
FindSingleElimination("should find previous matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
describe("Find previous and next matches in single elimination", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
const beforeFirst = manager.find.previousMatches(0);
|
||||
assert.equal(beforeFirst.length, 0);
|
||||
test("should find previous matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
});
|
||||
|
||||
const beforeSemi1 = manager.find.previousMatches(4);
|
||||
assert.equal(beforeSemi1.length, 2);
|
||||
assert.equal(beforeSemi1[0].id, 0);
|
||||
assert.equal(beforeSemi1[1].id, 1);
|
||||
const beforeFirst = manager.find.previousMatches(0);
|
||||
expect(beforeFirst.length).toBe(0);
|
||||
|
||||
const beforeSemi2 = manager.find.previousMatches(5);
|
||||
assert.equal(beforeSemi2.length, 2);
|
||||
assert.equal(beforeSemi2[0].id, 2);
|
||||
assert.equal(beforeSemi2[1].id, 3);
|
||||
const beforeSemi1 = manager.find.previousMatches(4);
|
||||
expect(beforeSemi1.length).toBe(2);
|
||||
expect(beforeSemi1[0].id).toBe(0);
|
||||
expect(beforeSemi1[1].id).toBe(1);
|
||||
|
||||
const beforeFinal = manager.find.previousMatches(6);
|
||||
assert.equal(beforeFinal.length, 2);
|
||||
assert.equal(beforeFinal[0].id, 4);
|
||||
assert.equal(beforeFinal[1].id, 5);
|
||||
});
|
||||
const beforeSemi2 = manager.find.previousMatches(5);
|
||||
expect(beforeSemi2.length).toBe(2);
|
||||
expect(beforeSemi2[0].id).toBe(2);
|
||||
expect(beforeSemi2[1].id).toBe(3);
|
||||
|
||||
FindSingleElimination("should find next matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
const beforeFinal = manager.find.previousMatches(6);
|
||||
expect(beforeFinal.length).toBe(2);
|
||||
expect(beforeFinal[0].id).toBe(4);
|
||||
expect(beforeFinal[1].id).toBe(5);
|
||||
});
|
||||
|
||||
const afterFirst = manager.find.nextMatches(0);
|
||||
assert.equal(afterFirst.length, 1);
|
||||
assert.equal(afterFirst[0].id, 4);
|
||||
test("should find next matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
});
|
||||
|
||||
const afterSemi1 = manager.find.nextMatches(4);
|
||||
assert.equal(afterSemi1.length, 1);
|
||||
assert.equal(afterSemi1[0].id, 6);
|
||||
const afterFirst = manager.find.nextMatches(0);
|
||||
expect(afterFirst.length).toBe(1);
|
||||
expect(afterFirst[0].id).toBe(4);
|
||||
|
||||
const afterFinal = manager.find.nextMatches(6);
|
||||
assert.equal(afterFinal.length, 0);
|
||||
});
|
||||
const afterSemi1 = manager.find.nextMatches(4);
|
||||
expect(afterSemi1.length).toBe(1);
|
||||
expect(afterSemi1[0].id).toBe(6);
|
||||
|
||||
FindSingleElimination(
|
||||
"should return matches from the point of view of a participant",
|
||||
() => {
|
||||
const afterFinal = manager.find.nextMatches(6);
|
||||
expect(afterFinal.length).toBe(0);
|
||||
});
|
||||
|
||||
test("should return matches from the point of view of a participant", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -76,117 +70,112 @@ FindSingleElimination(
|
||||
|
||||
manager.update.match({ id: 0, opponent1: { result: "loss" } });
|
||||
const afterFirstEliminated = manager.find.nextMatches(0, 1);
|
||||
assert.equal(afterFirstEliminated.length, 0);
|
||||
expect(afterFirstEliminated.length).toBe(0);
|
||||
const afterFirstContinued = manager.find.nextMatches(0, 2);
|
||||
assert.equal(afterFirstContinued.length, 1);
|
||||
expect(afterFirstContinued.length).toBe(1);
|
||||
|
||||
manager.update.match({ id: 1, opponent1: { result: "win" } });
|
||||
const beforeSemi1Up = manager.find.previousMatches(4, 2);
|
||||
assert.equal(beforeSemi1Up.length, 1);
|
||||
assert.equal(beforeSemi1Up[0].id, 0);
|
||||
expect(beforeSemi1Up.length).toBe(1);
|
||||
expect(beforeSemi1Up[0].id).toBe(0);
|
||||
|
||||
const beforeSemi1Down = manager.find.previousMatches(4, 3);
|
||||
assert.equal(beforeSemi1Down.length, 1);
|
||||
assert.equal(beforeSemi1Down[0].id, 1);
|
||||
},
|
||||
);
|
||||
|
||||
const FindDoubleElimination = suite(
|
||||
"Find previous and next matches in double elimination",
|
||||
);
|
||||
|
||||
FindDoubleElimination.before.each(() => {
|
||||
storage.reset();
|
||||
expect(beforeSemi1Down.length).toBe(1);
|
||||
expect(beforeSemi1Down[0].id).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
FindDoubleElimination("should find previous matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
describe("Find previous and next matches in double elimination", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
const beforeFirstWB = manager.find.previousMatches(0);
|
||||
assert.equal(beforeFirstWB.length, 0);
|
||||
test("should find previous matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
});
|
||||
|
||||
const beforeSemi1WB = manager.find.previousMatches(4);
|
||||
assert.equal(beforeSemi1WB.length, 2);
|
||||
assert.equal(beforeSemi1WB[0].id, 0);
|
||||
assert.equal(beforeSemi1WB[1].id, 1);
|
||||
const beforeFirstWB = manager.find.previousMatches(0);
|
||||
expect(beforeFirstWB.length).toBe(0);
|
||||
|
||||
const beforeSemi2WB = manager.find.previousMatches(5);
|
||||
assert.equal(beforeSemi2WB.length, 2);
|
||||
assert.equal(beforeSemi2WB[0].id, 2);
|
||||
assert.equal(beforeSemi2WB[1].id, 3);
|
||||
const beforeSemi1WB = manager.find.previousMatches(4);
|
||||
expect(beforeSemi1WB.length).toBe(2);
|
||||
expect(beforeSemi1WB[0].id).toBe(0);
|
||||
expect(beforeSemi1WB[1].id).toBe(1);
|
||||
|
||||
const beforeFinalWB = manager.find.previousMatches(6);
|
||||
assert.equal(beforeFinalWB.length, 2);
|
||||
assert.equal(beforeFinalWB[0].id, 4);
|
||||
assert.equal(beforeFinalWB[1].id, 5);
|
||||
const beforeSemi2WB = manager.find.previousMatches(5);
|
||||
expect(beforeSemi2WB.length).toBe(2);
|
||||
expect(beforeSemi2WB[0].id).toBe(2);
|
||||
expect(beforeSemi2WB[1].id).toBe(3);
|
||||
|
||||
const beforeFirstRound1LB = manager.find.previousMatches(7);
|
||||
assert.equal(beforeFirstRound1LB.length, 2);
|
||||
assert.equal(beforeFirstRound1LB[0].id, 0);
|
||||
assert.equal(beforeFirstRound1LB[1].id, 1);
|
||||
const beforeFinalWB = manager.find.previousMatches(6);
|
||||
expect(beforeFinalWB.length).toBe(2);
|
||||
expect(beforeFinalWB[0].id).toBe(4);
|
||||
expect(beforeFinalWB[1].id).toBe(5);
|
||||
|
||||
const beforeFirstRound2LB = manager.find.previousMatches(9);
|
||||
assert.equal(beforeFirstRound2LB.length, 2);
|
||||
assert.equal(beforeFirstRound2LB[0].id, 5);
|
||||
assert.equal(beforeFirstRound2LB[1].id, 7);
|
||||
const beforeFirstRound1LB = manager.find.previousMatches(7);
|
||||
expect(beforeFirstRound1LB.length).toBe(2);
|
||||
expect(beforeFirstRound1LB[0].id).toBe(0);
|
||||
expect(beforeFirstRound1LB[1].id).toBe(1);
|
||||
|
||||
const beforeSemi1LB = manager.find.previousMatches(11);
|
||||
assert.equal(beforeSemi1LB.length, 2);
|
||||
assert.equal(beforeSemi1LB[0].id, 9);
|
||||
assert.equal(beforeSemi1LB[1].id, 10);
|
||||
const beforeFirstRound2LB = manager.find.previousMatches(9);
|
||||
expect(beforeFirstRound2LB.length).toBe(2);
|
||||
expect(beforeFirstRound2LB[0].id).toBe(5);
|
||||
expect(beforeFirstRound2LB[1].id).toBe(7);
|
||||
|
||||
const beforeFinalLB = manager.find.previousMatches(12);
|
||||
assert.equal(beforeFinalLB.length, 2);
|
||||
assert.equal(beforeFinalLB[0].id, 6);
|
||||
assert.equal(beforeFinalLB[1].id, 11);
|
||||
});
|
||||
const beforeSemi1LB = manager.find.previousMatches(11);
|
||||
expect(beforeSemi1LB.length).toBe(2);
|
||||
expect(beforeSemi1LB[0].id).toBe(9);
|
||||
expect(beforeSemi1LB[1].id).toBe(10);
|
||||
|
||||
FindDoubleElimination("should find next matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
const beforeFinalLB = manager.find.previousMatches(12);
|
||||
expect(beforeFinalLB.length).toBe(2);
|
||||
expect(beforeFinalLB[0].id).toBe(6);
|
||||
expect(beforeFinalLB[1].id).toBe(11);
|
||||
});
|
||||
|
||||
const afterFirstWB = manager.find.nextMatches(0);
|
||||
assert.equal(afterFirstWB.length, 2);
|
||||
assert.equal(afterFirstWB[0].id, 4);
|
||||
assert.equal(afterFirstWB[1].id, 7);
|
||||
test("should find next matches", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
});
|
||||
|
||||
const afterSemi1WB = manager.find.nextMatches(4);
|
||||
assert.equal(afterSemi1WB.length, 2);
|
||||
assert.equal(afterSemi1WB[0].id, 6);
|
||||
assert.equal(afterSemi1WB[1].id, 10);
|
||||
const afterFirstWB = manager.find.nextMatches(0);
|
||||
expect(afterFirstWB.length).toBe(2);
|
||||
expect(afterFirstWB[0].id).toBe(4);
|
||||
expect(afterFirstWB[1].id).toBe(7);
|
||||
|
||||
const afterFinalWB = manager.find.nextMatches(6);
|
||||
assert.equal(afterFinalWB.length, 1);
|
||||
assert.equal(afterFinalWB[0].id, 12);
|
||||
const afterSemi1WB = manager.find.nextMatches(4);
|
||||
expect(afterSemi1WB.length).toBe(2);
|
||||
expect(afterSemi1WB[0].id).toBe(6);
|
||||
expect(afterSemi1WB[1].id).toBe(10);
|
||||
|
||||
const afterFirstRound1LB = manager.find.nextMatches(7);
|
||||
assert.equal(afterFirstRound1LB.length, 1);
|
||||
assert.equal(afterFirstRound1LB[0].id, 9);
|
||||
const afterFinalWB = manager.find.nextMatches(6);
|
||||
expect(afterFinalWB.length).toBe(1);
|
||||
expect(afterFinalWB[0].id).toBe(12);
|
||||
|
||||
const afterFirstRound2LB = manager.find.nextMatches(9);
|
||||
assert.equal(afterFirstRound2LB.length, 1);
|
||||
assert.equal(afterFirstRound2LB[0].id, 11);
|
||||
const afterFirstRound1LB = manager.find.nextMatches(7);
|
||||
expect(afterFirstRound1LB.length).toBe(1);
|
||||
expect(afterFirstRound1LB[0].id).toBe(9);
|
||||
|
||||
const afterSemi1LB = manager.find.nextMatches(11);
|
||||
assert.equal(afterSemi1LB.length, 1);
|
||||
assert.equal(afterSemi1LB[0].id, 12);
|
||||
const afterFirstRound2LB = manager.find.nextMatches(9);
|
||||
expect(afterFirstRound2LB.length).toBe(1);
|
||||
expect(afterFirstRound2LB[0].id).toBe(11);
|
||||
|
||||
const afterFinalLB = manager.find.nextMatches(12);
|
||||
assert.equal(afterFinalLB.length, 0);
|
||||
});
|
||||
const afterSemi1LB = manager.find.nextMatches(11);
|
||||
expect(afterSemi1LB.length).toBe(1);
|
||||
expect(afterSemi1LB[0].id).toBe(12);
|
||||
|
||||
FindDoubleElimination(
|
||||
"should return matches from the point of view of a participant",
|
||||
() => {
|
||||
const afterFinalLB = manager.find.nextMatches(12);
|
||||
expect(afterFinalLB.length).toBe(0);
|
||||
});
|
||||
|
||||
test("should return matches from the point of view of a participant", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -199,34 +188,30 @@ FindDoubleElimination(
|
||||
|
||||
manager.update.match({ id: 0, opponent1: { result: "loss" } });
|
||||
const afterFirstEliminated = manager.find.nextMatches(0, 1);
|
||||
assert.equal(afterFirstEliminated.length, 1);
|
||||
assert.equal(afterFirstEliminated[0].id, 3);
|
||||
expect(afterFirstEliminated.length).toBe(1);
|
||||
expect(afterFirstEliminated[0].id).toBe(3);
|
||||
const afterFirstContinued = manager.find.nextMatches(0, 2);
|
||||
assert.equal(afterFirstContinued.length, 1);
|
||||
assert.equal(afterFirstContinued[0].id, 2);
|
||||
expect(afterFirstContinued.length).toBe(1);
|
||||
expect(afterFirstContinued[0].id).toBe(2);
|
||||
|
||||
manager.update.match({ id: 1, opponent1: { result: "win" } });
|
||||
const beforeSemi1Up = manager.find.previousMatches(2, 2);
|
||||
assert.equal(beforeSemi1Up.length, 1);
|
||||
assert.equal(beforeSemi1Up[0].id, 0);
|
||||
expect(beforeSemi1Up.length).toBe(1);
|
||||
expect(beforeSemi1Up[0].id).toBe(0);
|
||||
|
||||
const beforeSemi1Down = manager.find.previousMatches(2, 3);
|
||||
assert.equal(beforeSemi1Down.length, 1);
|
||||
assert.equal(beforeSemi1Down[0].id, 1);
|
||||
expect(beforeSemi1Down.length).toBe(1);
|
||||
expect(beforeSemi1Down[0].id).toBe(1);
|
||||
|
||||
manager.update.match({ id: 3, opponent1: { result: "loss" } });
|
||||
const afterLowerBracketEliminated = manager.find.nextMatches(3, 1);
|
||||
assert.equal(afterLowerBracketEliminated.length, 0);
|
||||
expect(afterLowerBracketEliminated.length).toBe(0);
|
||||
const afterLowerBracketContinued = manager.find.nextMatches(3, 4);
|
||||
assert.equal(afterLowerBracketContinued.length, 1);
|
||||
assert.equal(afterLowerBracketContinued[0].id, 4);
|
||||
expect(afterLowerBracketContinued.length).toBe(1);
|
||||
expect(afterLowerBracketContinued[0].id).toBe(4);
|
||||
|
||||
assert.throws(
|
||||
() => manager.find.nextMatches(3, 42),
|
||||
expect(() => manager.find.nextMatches(3, 42)).toThrow(
|
||||
"The participant does not belong to this match.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FindSingleElimination.run();
|
||||
FindDoubleElimination.run();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,315 +1,279 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const BYEHandling = suite("BYE handling");
|
||||
|
||||
BYEHandling.before.each(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
BYEHandling("should propagate BYEs through the brackets", () => {
|
||||
manager.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, null, null, null],
|
||||
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
|
||||
describe("BYE handling", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 2).opponent1.id, 1);
|
||||
assert.equal(storage.select<any>("match", 2).opponent2, null);
|
||||
test("should propagate BYEs through the brackets", () => {
|
||||
manager.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, null, null, null],
|
||||
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 3).opponent1, null);
|
||||
assert.equal(storage.select<any>("match", 3).opponent2, null);
|
||||
expect(storage.select<any>("match", 2).opponent1.id).toBe(1);
|
||||
expect(storage.select<any>("match", 2).opponent2).toBe(null);
|
||||
|
||||
assert.equal(storage.select<any>("match", 4).opponent1, null);
|
||||
assert.equal(storage.select<any>("match", 4).opponent2, null);
|
||||
expect(storage.select<any>("match", 3).opponent1).toBe(null);
|
||||
expect(storage.select<any>("match", 3).opponent2).toBe(null);
|
||||
|
||||
assert.equal(storage.select<any>("match", 5).opponent1.id, 1);
|
||||
assert.equal(storage.select<any>("match", 5).opponent2, null);
|
||||
});
|
||||
expect(storage.select<any>("match", 4).opponent1).toBe(null);
|
||||
expect(storage.select<any>("match", 4).opponent2).toBe(null);
|
||||
|
||||
BYEHandling("should handle incomplete seeding during creation", () => {
|
||||
manager.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
balanceByes: false, // Default value.
|
||||
size: 4,
|
||||
},
|
||||
expect(storage.select<any>("match", 5).opponent1.id).toBe(1);
|
||||
expect(storage.select<any>("match", 5).opponent2).toBe(null);
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
|
||||
assert.equal(storage.select<any>("match", 0).opponent2.id, 2);
|
||||
test("should handle incomplete seeding during creation", () => {
|
||||
manager.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
balanceByes: false, // Default value.
|
||||
size: 4,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 1).opponent1, null);
|
||||
assert.equal(storage.select<any>("match", 1).opponent2, null);
|
||||
});
|
||||
expect(storage.select<any>("match", 0).opponent1.id).toBe(1);
|
||||
expect(storage.select<any>("match", 0).opponent2.id).toBe(2);
|
||||
|
||||
BYEHandling("should balance BYEs in the seeding", () => {
|
||||
manager.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
balanceByes: true,
|
||||
size: 4,
|
||||
},
|
||||
expect(storage.select<any>("match", 1).opponent1).toBe(null);
|
||||
expect(storage.select<any>("match", 1).opponent2).toBe(null);
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
|
||||
assert.equal(storage.select<any>("match", 0).opponent2, null);
|
||||
test("should balance BYEs in the seeding", () => {
|
||||
manager.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
balanceByes: true,
|
||||
size: 4,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 1).opponent1.id, 2);
|
||||
assert.equal(storage.select<any>("match", 1).opponent2, null);
|
||||
});
|
||||
expect(storage.select<any>("match", 0).opponent1.id).toBe(1);
|
||||
expect(storage.select<any>("match", 0).opponent2).toBe(null);
|
||||
|
||||
const PositionChecks = suite("Position checks");
|
||||
|
||||
PositionChecks.before.each(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Example with double grand final",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
settings: {
|
||||
size: 8,
|
||||
grandFinal: "simple",
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
expect(storage.select<any>("match", 1).opponent1.id).toBe(2);
|
||||
expect(storage.select<any>("match", 1).opponent2).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
PositionChecks(
|
||||
"should not have a position when we don't need the origin of a participant",
|
||||
() => {
|
||||
describe("Position checks", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Example with double grand final",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
settings: {
|
||||
size: 8,
|
||||
grandFinal: "simple",
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should not have a position when we don't need the origin of a participant", () => {
|
||||
const matchFromWbRound2 = storage.select<any>("match", 4);
|
||||
assert.equal(matchFromWbRound2.opponent1.position, undefined);
|
||||
assert.equal(matchFromWbRound2.opponent2.position, undefined);
|
||||
expect(matchFromWbRound2.opponent1.position).toBe(undefined);
|
||||
expect(matchFromWbRound2.opponent2.position).toBe(undefined);
|
||||
|
||||
const matchFromLbRound2 = storage.select<any>("match", 9);
|
||||
assert.equal(matchFromLbRound2.opponent2.position, undefined);
|
||||
expect(matchFromLbRound2.opponent2.position).toBe(undefined);
|
||||
|
||||
const matchFromGrandFinal = storage.select<any>("match", 13);
|
||||
assert.equal(matchFromGrandFinal.opponent1.position, undefined);
|
||||
},
|
||||
);
|
||||
expect(matchFromGrandFinal.opponent1.position).toBe(undefined);
|
||||
});
|
||||
|
||||
PositionChecks(
|
||||
"should have a position where we need the origin of a participant",
|
||||
() => {
|
||||
test("should have a position where we need the origin of a participant", () => {
|
||||
const matchFromWbRound1 = storage.select<any>("match", 0);
|
||||
assert.equal(matchFromWbRound1.opponent1.position, 1);
|
||||
assert.equal(matchFromWbRound1.opponent2.position, 2);
|
||||
expect(matchFromWbRound1.opponent1.position).toBe(1);
|
||||
expect(matchFromWbRound1.opponent2.position).toBe(2);
|
||||
|
||||
const matchFromLbRound1 = storage.select<any>("match", 7);
|
||||
assert.equal(matchFromLbRound1.opponent1.position, 1);
|
||||
assert.equal(matchFromLbRound1.opponent2.position, 2);
|
||||
expect(matchFromLbRound1.opponent1.position).toBe(1);
|
||||
expect(matchFromLbRound1.opponent2.position).toBe(2);
|
||||
|
||||
const matchFromLbRound2 = storage.select<any>("match", 9);
|
||||
assert.equal(matchFromLbRound2.opponent1.position, 2);
|
||||
expect(matchFromLbRound2.opponent1.position).toBe(2);
|
||||
|
||||
const matchFromGrandFinal = storage.select<any>("match", 13);
|
||||
assert.equal(matchFromGrandFinal.opponent2.position, 1);
|
||||
},
|
||||
);
|
||||
|
||||
const SpecialCases = suite("Special cases");
|
||||
|
||||
SpecialCases.before.each(() => {
|
||||
storage.reset();
|
||||
expect(matchFromGrandFinal.opponent2.position).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
SpecialCases("should throw if the name of the stage is not provided", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
describe("Special cases", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
test("should throw if the name of the stage is not provided", () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error testing throwing
|
||||
manager.create({
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
}),
|
||||
"You must provide a name for the stage.",
|
||||
);
|
||||
});
|
||||
).toThrow("You must provide a name for the stage.");
|
||||
});
|
||||
|
||||
SpecialCases(
|
||||
"should throw if the tournament id of the stage is not provided",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
// @ts-expect-error testing throwing
|
||||
manager.create({
|
||||
name: "Example",
|
||||
type: "single_elimination",
|
||||
}),
|
||||
"You must provide a tournament id for the stage.",
|
||||
);
|
||||
},
|
||||
);
|
||||
test("should throw if the tournament id of the stage is not provided", () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error testing throwing
|
||||
manager.create({
|
||||
name: "Example",
|
||||
type: "single_elimination",
|
||||
}),
|
||||
).toThrow("You must provide a tournament id for the stage.");
|
||||
});
|
||||
|
||||
SpecialCases(
|
||||
"should throw if the participant count of a stage is not a power of two",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7],
|
||||
}),
|
||||
test("should throw if the participant count of a stage is not a power of two", () => {
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7],
|
||||
}),
|
||||
).toThrow(
|
||||
"The library only supports a participant count which is a power of two.",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 3 },
|
||||
}),
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 3 },
|
||||
}),
|
||||
).toThrow(
|
||||
"The library only supports a participant count which is a power of two.",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
SpecialCases(
|
||||
"should throw if the participant count of a stage is less than two",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 0 },
|
||||
}),
|
||||
test("should throw if the participant count of a stage is less than two", () => {
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 0 },
|
||||
}),
|
||||
).toThrow(
|
||||
"Impossible to create an empty stage. If you want an empty seeding, just set the size of the stage.",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 1 },
|
||||
}),
|
||||
"Impossible to create a stage with less than 2 participants.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const SeedingAndOrderingInElimination = suite(
|
||||
"Seeding and ordering in elimination",
|
||||
);
|
||||
|
||||
SeedingAndOrderingInElimination.before.each(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: {
|
||||
seedOrdering: [
|
||||
"inner_outer",
|
||||
"reverse",
|
||||
"pair_flip",
|
||||
"half_shift",
|
||||
"reverse",
|
||||
],
|
||||
},
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 1 },
|
||||
}),
|
||||
).toThrow("Impossible to create a stage with less than 2 participants.");
|
||||
});
|
||||
});
|
||||
|
||||
SeedingAndOrderingInElimination(
|
||||
"should have the good orderings everywhere",
|
||||
() => {
|
||||
describe("Seeding and ordering in elimination", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: {
|
||||
seedOrdering: [
|
||||
"inner_outer",
|
||||
"reverse",
|
||||
"pair_flip",
|
||||
"half_shift",
|
||||
"reverse",
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should have the good orderings everywhere", () => {
|
||||
const firstRoundMatchWB = storage.select<any>("match", 0);
|
||||
assert.equal(firstRoundMatchWB.opponent1.position, 1);
|
||||
assert.equal(firstRoundMatchWB.opponent2.position, 16);
|
||||
expect(firstRoundMatchWB.opponent1.position).toBe(1);
|
||||
expect(firstRoundMatchWB.opponent2.position).toBe(16);
|
||||
|
||||
const firstRoundMatchLB = storage.select<any>("match", 15);
|
||||
assert.equal(firstRoundMatchLB.opponent1.position, 8);
|
||||
assert.equal(firstRoundMatchLB.opponent2.position, 7);
|
||||
expect(firstRoundMatchLB.opponent1.position).toBe(8);
|
||||
expect(firstRoundMatchLB.opponent2.position).toBe(7);
|
||||
|
||||
const secondRoundMatchLB = storage.select<any>("match", 19);
|
||||
assert.equal(secondRoundMatchLB.opponent1.position, 2);
|
||||
expect(secondRoundMatchLB.opponent1.position).toBe(2);
|
||||
|
||||
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
|
||||
assert.equal(secondRoundSecondMatchLB.opponent1.position, 1);
|
||||
expect(secondRoundSecondMatchLB.opponent1.position).toBe(1);
|
||||
|
||||
const fourthRoundMatchLB = storage.select<any>("match", 25);
|
||||
assert.equal(fourthRoundMatchLB.opponent1.position, 2);
|
||||
expect(fourthRoundMatchLB.opponent1.position).toBe(2);
|
||||
|
||||
const finalRoundMatchLB = storage.select<any>("match", 28);
|
||||
assert.equal(finalRoundMatchLB.opponent1.position, 1);
|
||||
},
|
||||
);
|
||||
expect(finalRoundMatchLB.opponent1.position).toBe(1);
|
||||
});
|
||||
|
||||
SeedingAndOrderingInElimination("should update the orderings in rounds", () => {
|
||||
let firstRoundMatchWB = storage.select<any>("match", 0);
|
||||
test("should update the orderings in rounds", () => {
|
||||
let firstRoundMatchWB = storage.select<any>("match", 0);
|
||||
|
||||
// Inner outer before changing.
|
||||
assert.equal(firstRoundMatchWB.opponent1.position, 1);
|
||||
assert.equal(firstRoundMatchWB.opponent2.position, 16);
|
||||
// Inner outer before changing.
|
||||
expect(firstRoundMatchWB.opponent1.position).toBe(1);
|
||||
expect(firstRoundMatchWB.opponent2.position).toBe(16);
|
||||
|
||||
manager.update.roundOrdering(0, "pair_flip");
|
||||
manager.update.roundOrdering(0, "pair_flip");
|
||||
|
||||
firstRoundMatchWB = storage.select<any>("match", 0);
|
||||
firstRoundMatchWB = storage.select<any>("match", 0);
|
||||
|
||||
// Should now be pair_flip.
|
||||
assert.equal(firstRoundMatchWB.opponent1.position, 2);
|
||||
assert.equal(firstRoundMatchWB.opponent2.position, 1);
|
||||
// Should now be pair_flip.
|
||||
expect(firstRoundMatchWB.opponent1.position).toBe(2);
|
||||
expect(firstRoundMatchWB.opponent2.position).toBe(1);
|
||||
|
||||
manager.update.roundOrdering(5, "reverse");
|
||||
manager.update.roundOrdering(5, "reverse");
|
||||
|
||||
const secondRoundMatchLB = storage.select<any>("match", 19);
|
||||
assert.equal(secondRoundMatchLB.opponent1.position, 4);
|
||||
const secondRoundMatchLB = storage.select<any>("match", 19);
|
||||
expect(secondRoundMatchLB.opponent1.position).toBe(4);
|
||||
|
||||
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
|
||||
assert.equal(secondRoundSecondMatchLB.opponent1.position, 3);
|
||||
});
|
||||
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
|
||||
expect(secondRoundSecondMatchLB.opponent1.position).toBe(3);
|
||||
});
|
||||
|
||||
SeedingAndOrderingInElimination(
|
||||
"should throw if round does not support ordering",
|
||||
() => {
|
||||
assert.throws(
|
||||
() => manager.update.roundOrdering(6, "natural"), // LB Round 2
|
||||
test("should throw if round does not support ordering", () => {
|
||||
expect(() => manager.update.roundOrdering(6, "natural")).toThrow(
|
||||
"This round does not support ordering.",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => manager.update.roundOrdering(9, "natural"), // LB Round 6 (last minor round)
|
||||
expect(() => manager.update.roundOrdering(9, "natural")).toThrow(
|
||||
"This round does not support ordering.",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
SeedingAndOrderingInElimination(
|
||||
"should throw if at least one match is running or completed",
|
||||
() => {
|
||||
test("should throw if at least one match is running or completed", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 1 },
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => manager.update.roundOrdering(0, "natural"),
|
||||
expect(() => manager.update.roundOrdering(0, "natural")).toThrow(
|
||||
"At least one match has started or is completed.",
|
||||
);
|
||||
|
||||
@@ -318,16 +282,12 @@ SeedingAndOrderingInElimination(
|
||||
opponent1: { result: "win" },
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => manager.update.roundOrdering(0, "natural"),
|
||||
expect(() => manager.update.roundOrdering(0, "natural")).toThrow(
|
||||
"At least one match has started or is completed.",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
SeedingAndOrderingInElimination(
|
||||
"should update all the ordering of a stage at once",
|
||||
() => {
|
||||
test("should update all the ordering of a stage at once", () => {
|
||||
manager.update.ordering(0, [
|
||||
"pair_flip",
|
||||
"half_shift",
|
||||
@@ -336,83 +296,80 @@ SeedingAndOrderingInElimination(
|
||||
]);
|
||||
|
||||
const firstRoundMatchWB = storage.select<any>("match", 0);
|
||||
assert.equal(firstRoundMatchWB.opponent1.position, 2);
|
||||
assert.equal(firstRoundMatchWB.opponent2.position, 1);
|
||||
expect(firstRoundMatchWB.opponent1.position).toBe(2);
|
||||
expect(firstRoundMatchWB.opponent2.position).toBe(1);
|
||||
|
||||
const firstRoundMatchLB = storage.select<any>("match", 15);
|
||||
assert.equal(firstRoundMatchLB.opponent1.position, 5);
|
||||
assert.equal(firstRoundMatchLB.opponent2.position, 6);
|
||||
expect(firstRoundMatchLB.opponent1.position).toBe(5);
|
||||
expect(firstRoundMatchLB.opponent2.position).toBe(6);
|
||||
|
||||
const secondRoundMatchLB = storage.select<any>("match", 19);
|
||||
assert.equal(secondRoundMatchLB.opponent1.position, 4);
|
||||
expect(secondRoundMatchLB.opponent1.position).toBe(4);
|
||||
|
||||
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
|
||||
assert.equal(secondRoundSecondMatchLB.opponent1.position, 3);
|
||||
expect(secondRoundSecondMatchLB.opponent1.position).toBe(3);
|
||||
|
||||
const fourthRoundMatchLB = storage.select<any>("match", 25);
|
||||
assert.equal(fourthRoundMatchLB.opponent1.position, 1);
|
||||
expect(fourthRoundMatchLB.opponent1.position).toBe(1);
|
||||
|
||||
const finalRoundMatchLB = storage.select<any>("match", 28);
|
||||
assert.equal(finalRoundMatchLB.opponent1.position, 1);
|
||||
},
|
||||
);
|
||||
|
||||
const ResetMatchAndMatchGames = suite("Reset match and match games");
|
||||
|
||||
ResetMatchAndMatchGames.before.each(() => {
|
||||
storage.reset();
|
||||
expect(finalRoundMatchLB.opponent1.position).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
ResetMatchAndMatchGames("should reset results of a match", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
size: 8,
|
||||
},
|
||||
describe("Reset match and match games", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
test("should reset results of a match", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
size: 8,
|
||||
},
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
});
|
||||
|
||||
let match = storage.select<any>("match", 0);
|
||||
expect(match.opponent1.score).toBe(16);
|
||||
expect(match.opponent2.score).toBe(12);
|
||||
expect(match.opponent1.result).toBe("win");
|
||||
|
||||
let semi1 = storage.select<any>("match", 4);
|
||||
expect(semi1.opponent1.result).toBe("win");
|
||||
expect(semi1.opponent2).toBe(null);
|
||||
|
||||
let final = storage.select<any>("match", 6);
|
||||
expect(final.opponent1.result).toBe("win");
|
||||
expect(final.opponent2).toBe(null);
|
||||
|
||||
manager.reset.matchResults(0); // Score stays as is.
|
||||
|
||||
match = storage.select<any>("match", 0);
|
||||
expect(match.opponent1.score).toBe(16);
|
||||
expect(match.opponent2.score).toBe(12);
|
||||
expect(match.opponent1.result).toBe(undefined);
|
||||
|
||||
semi1 = storage.select<any>("match", 4);
|
||||
expect(semi1.opponent1.result).toBe(undefined);
|
||||
expect(semi1.opponent2).toBe(null);
|
||||
|
||||
final = storage.select<any>("match", 6);
|
||||
expect(final.opponent1.result).toBe(undefined);
|
||||
expect(final.opponent2).toBe(null);
|
||||
});
|
||||
|
||||
let match = storage.select<any>("match", 0);
|
||||
assert.equal(match.opponent1.score, 16);
|
||||
assert.equal(match.opponent2.score, 12);
|
||||
assert.equal(match.opponent1.result, "win");
|
||||
|
||||
let semi1 = storage.select<any>("match", 4);
|
||||
assert.equal(semi1.opponent1.result, "win");
|
||||
assert.equal(semi1.opponent2, null);
|
||||
|
||||
let final = storage.select<any>("match", 6);
|
||||
assert.equal(final.opponent1.result, "win");
|
||||
assert.equal(final.opponent2, null);
|
||||
|
||||
manager.reset.matchResults(0); // Score stays as is.
|
||||
|
||||
match = storage.select<any>("match", 0);
|
||||
assert.equal(match.opponent1.score, 16);
|
||||
assert.equal(match.opponent2.score, 12);
|
||||
assert.equal(match.opponent1.result, undefined);
|
||||
|
||||
semi1 = storage.select<any>("match", 4);
|
||||
assert.equal(semi1.opponent1.result, undefined);
|
||||
assert.equal(semi1.opponent2, null);
|
||||
|
||||
final = storage.select<any>("match", 6);
|
||||
assert.equal(final.opponent1.result, undefined);
|
||||
assert.equal(final.opponent2, null);
|
||||
});
|
||||
|
||||
ResetMatchAndMatchGames(
|
||||
"should throw when at least one of the following match is locked",
|
||||
() => {
|
||||
test("should throw when at least one of the following match is locked", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -441,82 +398,75 @@ ResetMatchAndMatchGames(
|
||||
opponent2: { score: 12 },
|
||||
});
|
||||
|
||||
assert.throws(() => manager.reset.matchResults(0), "The match is locked.");
|
||||
},
|
||||
);
|
||||
|
||||
const ImportExport = suite("Import / export");
|
||||
|
||||
ImportExport.before.each(() => {
|
||||
storage.reset();
|
||||
expect(() => manager.reset.matchResults(0)).toThrow("The match is locked.");
|
||||
});
|
||||
});
|
||||
|
||||
ImportExport("should import data in the storage", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
describe("Import / export", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
const initialData = manager.get.stageData(0);
|
||||
test("should import data in the storage", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
const initialData = manager.get.stageData(0);
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 2,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
});
|
||||
|
||||
expect(storage.select<any>("match", 0).opponent1.result).toBe("win");
|
||||
expect(storage.select<any>("match", 1).opponent1.result).toBe("win");
|
||||
|
||||
manager.import(initialData);
|
||||
|
||||
expect(storage.select<any>("match", 0).opponent1.result).toBe(undefined);
|
||||
expect(storage.select<any>("match", 1).opponent1.result).toBe(undefined);
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
test("should export data from the storage", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
});
|
||||
|
||||
const data = manager.export();
|
||||
|
||||
for (const key of ["stage", "group", "round", "match"]) {
|
||||
expect(Object.keys(data).includes(key)).toBe(true);
|
||||
}
|
||||
|
||||
expect(storage.select<any>("stage")).toEqual(data.stage);
|
||||
expect(storage.select<any>("group")).toEqual(data.group);
|
||||
expect(storage.select<any>("round")).toEqual(data.round);
|
||||
expect(storage.select<any>("match")).toEqual(data.match);
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 2,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 0).opponent1.result, "win");
|
||||
assert.equal(storage.select<any>("match", 1).opponent1.result, "win");
|
||||
|
||||
manager.import(initialData);
|
||||
|
||||
assert.equal(storage.select<any>("match", 0).opponent1.result, undefined);
|
||||
assert.equal(storage.select<any>("match", 1).opponent1.result, undefined);
|
||||
});
|
||||
|
||||
ImportExport("should export data from the storage", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
});
|
||||
|
||||
const data = manager.export();
|
||||
|
||||
for (const key of ["stage", "group", "round", "match"]) {
|
||||
assert.ok(Object.keys(data).includes(key));
|
||||
}
|
||||
|
||||
assert.equal(storage.select<any>("stage"), data.stage);
|
||||
assert.equal(storage.select<any>("group"), data.group);
|
||||
assert.equal(storage.select<any>("round"), data.round);
|
||||
assert.equal(storage.select<any>("match"), data.match);
|
||||
});
|
||||
|
||||
BYEHandling.run();
|
||||
PositionChecks.run();
|
||||
SpecialCases.run();
|
||||
SeedingAndOrderingInElimination.run();
|
||||
ResetMatchAndMatchGames.run();
|
||||
ImportExport.run();
|
||||
|
||||
@@ -1,92 +1,89 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const GetSeeding = suite("Get seeding");
|
||||
describe("Get seeding", () => {
|
||||
test("should get the seeding of a round-robin stage", () => {
|
||||
storage.reset();
|
||||
|
||||
GetSeeding("should get the seeding of a round-robin stage", () => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
settings: {
|
||||
groupCount: 8,
|
||||
size: 32,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
settings: {
|
||||
groupCount: 8,
|
||||
size: 32,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
const seeding = manager.get.seeding(0);
|
||||
expect(seeding.length).toBe(32);
|
||||
expect(seeding[0]!.position).toBe(1);
|
||||
expect(seeding[1]!.position).toBe(2);
|
||||
});
|
||||
|
||||
const seeding = manager.get.seeding(0);
|
||||
assert.equal(seeding.length, 32);
|
||||
assert.equal(seeding[0]!.position, 1);
|
||||
assert.equal(seeding[1]!.position, 2);
|
||||
});
|
||||
test("should get the seeding of a round-robin stage with BYEs", () => {
|
||||
storage.reset();
|
||||
|
||||
GetSeeding("should get the seeding of a round-robin stage with BYEs", () => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
settings: {
|
||||
groupCount: 2,
|
||||
size: 8,
|
||||
},
|
||||
seeding: [1, null, null, null, null, null, null, null],
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
settings: {
|
||||
groupCount: 2,
|
||||
size: 8,
|
||||
},
|
||||
seeding: [1, null, null, null, null, null, null, null],
|
||||
const seeding = manager.get.seeding(0);
|
||||
expect(seeding.length).toBe(8);
|
||||
});
|
||||
|
||||
const seeding = manager.get.seeding(0);
|
||||
assert.equal(seeding.length, 8);
|
||||
});
|
||||
test("should get the seeding of a single elimination stage", () => {
|
||||
storage.reset();
|
||||
|
||||
GetSeeding("should get the seeding of a single elimination stage", () => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 16 },
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: { size: 16 },
|
||||
const seeding = manager.get.seeding(0);
|
||||
expect(seeding.length).toBe(16);
|
||||
expect(seeding[0]!.position).toBe(1);
|
||||
expect(seeding[1]!.position).toBe(2);
|
||||
});
|
||||
|
||||
const seeding = manager.get.seeding(0);
|
||||
assert.equal(seeding.length, 16);
|
||||
assert.equal(seeding[0]!.position, 1);
|
||||
assert.equal(seeding[1]!.position, 2);
|
||||
});
|
||||
test("should get the seeding with BYEs", () => {
|
||||
storage.reset();
|
||||
|
||||
GetSeeding("should get the seeding with BYEs", () => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, null, 2, 3, 4, null, null, 5],
|
||||
settings: {
|
||||
seedOrdering: ["inner_outer"],
|
||||
},
|
||||
});
|
||||
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, null, 2, 3, 4, null, null, 5],
|
||||
settings: {
|
||||
seedOrdering: ["inner_outer"],
|
||||
},
|
||||
const seeding = manager.get.seeding(0);
|
||||
expect(seeding.length).toBe(8);
|
||||
expect(seeding).toEqual([
|
||||
{ id: 1, position: 1 },
|
||||
null,
|
||||
{ id: 2, position: 3 },
|
||||
{ id: 3, position: 4 },
|
||||
{ id: 4, position: 5 },
|
||||
null,
|
||||
null,
|
||||
{ id: 5, position: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
const seeding = manager.get.seeding(0);
|
||||
assert.equal(seeding.length, 8);
|
||||
assert.equal(seeding, [
|
||||
{ id: 1, position: 1 },
|
||||
null,
|
||||
{ id: 2, position: 3 },
|
||||
{ id: 3, position: 4 },
|
||||
{ id: 4, position: 5 },
|
||||
null,
|
||||
null,
|
||||
{ id: 5, position: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
GetSeeding.run();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
assertRoundRobin,
|
||||
balanceByes,
|
||||
@@ -8,281 +7,152 @@ import {
|
||||
} from "../helpers";
|
||||
import { ordering } from "../ordering";
|
||||
|
||||
const RoundRobinGroups = suite("Round-robin groups");
|
||||
describe("Round-robin groups", () => {
|
||||
test("should place participants in groups", () => {
|
||||
expect(makeGroups([1, 2, 3, 4, 5], 2)).toEqual([
|
||||
[1, 2, 3],
|
||||
[4, 5],
|
||||
]);
|
||||
expect(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 2)).toEqual([
|
||||
[1, 2, 3, 4],
|
||||
[5, 6, 7, 8],
|
||||
]);
|
||||
expect(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 3)).toEqual([
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8],
|
||||
]);
|
||||
});
|
||||
|
||||
RoundRobinGroups("should place participants in groups", () => {
|
||||
assert.equal(makeGroups([1, 2, 3, 4, 5], 2), [
|
||||
[1, 2, 3],
|
||||
[4, 5],
|
||||
]);
|
||||
assert.equal(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 2), [
|
||||
[1, 2, 3, 4],
|
||||
[5, 6, 7, 8],
|
||||
]);
|
||||
assert.equal(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 3), [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8],
|
||||
]);
|
||||
test("should make the rounds for a round-robin group", () => {
|
||||
assertRoundRobin([1, 2, 3], makeRoundRobinMatches([1, 2, 3]));
|
||||
assertRoundRobin([1, 2, 3, 4], makeRoundRobinMatches([1, 2, 3, 4]));
|
||||
assertRoundRobin([1, 2, 3, 4, 5], makeRoundRobinMatches([1, 2, 3, 4, 5]));
|
||||
assertRoundRobin(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
makeRoundRobinMatches([1, 2, 3, 4, 5, 6]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
RoundRobinGroups("should make the rounds for a round-robin group", () => {
|
||||
assertRoundRobin([1, 2, 3], makeRoundRobinMatches([1, 2, 3]));
|
||||
assertRoundRobin([1, 2, 3, 4], makeRoundRobinMatches([1, 2, 3, 4]));
|
||||
assertRoundRobin([1, 2, 3, 4, 5], makeRoundRobinMatches([1, 2, 3, 4, 5]));
|
||||
assertRoundRobin(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
makeRoundRobinMatches([1, 2, 3, 4, 5, 6]),
|
||||
);
|
||||
});
|
||||
|
||||
const SeedOrderingMethods = suite("Seed ordering methods");
|
||||
|
||||
SeedOrderingMethods(
|
||||
"should place 2 participants with inner-outer method",
|
||||
() => {
|
||||
const teams = [1, 2]; // This is the minimum participant count supported by the library.
|
||||
describe("Seed ordering methods", () => {
|
||||
test("should place 2 participants with inner-outer method", () => {
|
||||
const teams = [1, 2];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
assert.equal(placement, [1, 2]);
|
||||
},
|
||||
);
|
||||
expect(placement).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods(
|
||||
"should place 4 participants with inner-outer method",
|
||||
() => {
|
||||
test("should place 4 participants with inner-outer method", () => {
|
||||
const teams = [1, 2, 3, 4];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
assert.equal(placement, [1, 4, 2, 3]);
|
||||
},
|
||||
);
|
||||
expect(placement).toEqual([1, 4, 2, 3]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods(
|
||||
"should place 8 participants with inner-outer method",
|
||||
() => {
|
||||
test("should place 8 participants with inner-outer method", () => {
|
||||
const teams = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
assert.equal(placement, [1, 8, 4, 5, 2, 7, 3, 6]);
|
||||
},
|
||||
);
|
||||
expect(placement).toEqual([1, 8, 4, 5, 2, 7, 3, 6]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods(
|
||||
"should place 16 participants with inner-outer method",
|
||||
() => {
|
||||
test("should place 16 participants with inner-outer method", () => {
|
||||
const teams = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
assert.equal(
|
||||
placement,
|
||||
[1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11],
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(placement).toEqual([
|
||||
1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11,
|
||||
]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods("should make a natural ordering", () => {
|
||||
assert.equal(
|
||||
ordering.natural([1, 2, 3, 4, 5, 6, 7, 8]),
|
||||
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||
);
|
||||
});
|
||||
test("should make a natural ordering", () => {
|
||||
expect(ordering.natural([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods("should make a reverse ordering", () => {
|
||||
assert.equal(
|
||||
ordering.reverse([1, 2, 3, 4, 5, 6, 7, 8]),
|
||||
[8, 7, 6, 5, 4, 3, 2, 1],
|
||||
);
|
||||
});
|
||||
test("should make a reverse ordering", () => {
|
||||
expect(ordering.reverse([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([
|
||||
8, 7, 6, 5, 4, 3, 2, 1,
|
||||
]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods("should make a half shift ordering", () => {
|
||||
assert.equal(
|
||||
ordering.half_shift([1, 2, 3, 4, 5, 6, 7, 8]),
|
||||
[5, 6, 7, 8, 1, 2, 3, 4],
|
||||
);
|
||||
});
|
||||
test("should make a half shift ordering", () => {
|
||||
expect(ordering.half_shift([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([
|
||||
5, 6, 7, 8, 1, 2, 3, 4,
|
||||
]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods("should make a reverse half shift ordering", () => {
|
||||
assert.equal(
|
||||
ordering.reverse_half_shift([1, 2, 3, 4, 5, 6, 7, 8]),
|
||||
[4, 3, 2, 1, 8, 7, 6, 5],
|
||||
);
|
||||
});
|
||||
test("should make a reverse half shift ordering", () => {
|
||||
expect(ordering.reverse_half_shift([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([
|
||||
4, 3, 2, 1, 8, 7, 6, 5,
|
||||
]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods("should make a pair flip ordering", () => {
|
||||
assert.equal(
|
||||
ordering.pair_flip([1, 2, 3, 4, 5, 6, 7, 8]),
|
||||
[2, 1, 4, 3, 6, 5, 8, 7],
|
||||
);
|
||||
});
|
||||
test("should make a pair flip ordering", () => {
|
||||
expect(ordering.pair_flip([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([
|
||||
2, 1, 4, 3, 6, 5, 8, 7,
|
||||
]);
|
||||
});
|
||||
|
||||
SeedOrderingMethods(
|
||||
"should make an effort balanced ordering for groups",
|
||||
() => {
|
||||
assert.equal(
|
||||
test("should make an effort balanced ordering for groups", () => {
|
||||
expect(
|
||||
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 4),
|
||||
[
|
||||
1,
|
||||
5, // 1st group
|
||||
2,
|
||||
6, // 2nd group
|
||||
3,
|
||||
7, // 3rd group
|
||||
4,
|
||||
8, // 4th group
|
||||
],
|
||||
);
|
||||
).toEqual([1, 5, 2, 6, 3, 7, 4, 8]);
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
ordering["groups.effort_balanced"](
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
4,
|
||||
),
|
||||
[
|
||||
1,
|
||||
5,
|
||||
9,
|
||||
13, // 1st group
|
||||
2,
|
||||
6,
|
||||
10,
|
||||
14, // 2nd group
|
||||
3,
|
||||
7,
|
||||
11,
|
||||
15, // 3rd group
|
||||
4,
|
||||
8,
|
||||
12,
|
||||
16, // 4th group
|
||||
],
|
||||
);
|
||||
).toEqual([1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 2),
|
||||
[
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
7, // 1st group
|
||||
2,
|
||||
).toEqual([1, 3, 5, 7, 2, 4, 6, 8]);
|
||||
});
|
||||
|
||||
test("should make a snake ordering for groups", () => {
|
||||
expect(
|
||||
ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 4),
|
||||
).toEqual([1, 8, 2, 7, 3, 6, 4, 5]);
|
||||
|
||||
expect(
|
||||
ordering["groups.seed_optimized"](
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
4,
|
||||
6,
|
||||
8, // 2nd group
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
),
|
||||
).toEqual([1, 8, 9, 16, 2, 7, 10, 15, 3, 6, 11, 14, 4, 5, 12, 13]);
|
||||
|
||||
SeedOrderingMethods("should make a snake ordering for groups", () => {
|
||||
assert.equal(ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 4), [
|
||||
1,
|
||||
8, // 1st group
|
||||
2,
|
||||
7, // 2nd group
|
||||
3,
|
||||
6, // 3rd group
|
||||
4,
|
||||
5, // 4th group
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
ordering["groups.seed_optimized"](
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
4,
|
||||
),
|
||||
[
|
||||
1,
|
||||
8,
|
||||
9,
|
||||
16, // 1st group
|
||||
2,
|
||||
7,
|
||||
10,
|
||||
15, // 2nd group
|
||||
3,
|
||||
6,
|
||||
11,
|
||||
14, // 3rd group
|
||||
4,
|
||||
5,
|
||||
12,
|
||||
13, // 4th group
|
||||
],
|
||||
);
|
||||
|
||||
assert.equal(ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 2), [
|
||||
1,
|
||||
4,
|
||||
5,
|
||||
8, // 1st group
|
||||
2,
|
||||
3,
|
||||
6,
|
||||
7, // 2nd group
|
||||
]);
|
||||
expect(
|
||||
ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 2),
|
||||
).toEqual([1, 4, 5, 8, 2, 3, 6, 7]);
|
||||
});
|
||||
});
|
||||
|
||||
const BalanceByes = suite("Balance BYEs");
|
||||
describe("Balance BYEs", () => {
|
||||
test("should ignore input BYEs in the seeding", () => {
|
||||
expect(
|
||||
balanceByes(
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null, null, null, null],
|
||||
16,
|
||||
),
|
||||
).toEqual(balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16));
|
||||
|
||||
BalanceByes("should ignore input BYEs in the seeding", () => {
|
||||
assert.equal(
|
||||
balanceByes(
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null, null, null, null],
|
||||
16,
|
||||
),
|
||||
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
|
||||
);
|
||||
expect(
|
||||
balanceByes(
|
||||
[1, 2, 3, null, 4, 5, 6, 7, 8, null, 9, 10, null, 11, null, 12, null],
|
||||
16,
|
||||
),
|
||||
).toEqual(balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16));
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
balanceByes(
|
||||
[1, 2, 3, null, 4, 5, 6, 7, 8, null, 9, 10, null, 11, null, 12, null],
|
||||
16,
|
||||
),
|
||||
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
|
||||
);
|
||||
});
|
||||
|
||||
BalanceByes(
|
||||
"should take the target size as an argument or calculate it",
|
||||
() => {
|
||||
assert.equal(
|
||||
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
|
||||
test("should take the target size as an argument or calculate it", () => {
|
||||
expect(balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16)).toEqual(
|
||||
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
BalanceByes("should prefer matches with only one BYE", () => {
|
||||
assert.equal(
|
||||
balanceByes([
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]),
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, null, 10, null, 11, null, 12, null],
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
balanceByes(
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, null, null, null, null, null, null, null, null],
|
||||
16,
|
||||
),
|
||||
[1, null, 2, null, 3, null, 4, null, 5, null, 6, null, 7, null, 8, null],
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
balanceByes(
|
||||
[
|
||||
test("should prefer matches with only one BYE", () => {
|
||||
expect(
|
||||
balanceByes([
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
@@ -290,22 +160,98 @@ BalanceByes("should prefer matches with only one BYE", () => {
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
16,
|
||||
),
|
||||
[1, null, 2, null, 3, null, 4, null, 5, null, 6, null, 7, null, null, null],
|
||||
);
|
||||
});
|
||||
]),
|
||||
).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, null, 10, null, 11, null, 12, null]);
|
||||
|
||||
RoundRobinGroups.run();
|
||||
SeedOrderingMethods.run();
|
||||
BalanceByes.run();
|
||||
expect(
|
||||
balanceByes(
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
16,
|
||||
),
|
||||
).toEqual([
|
||||
1,
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
3,
|
||||
null,
|
||||
4,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
6,
|
||||
null,
|
||||
7,
|
||||
null,
|
||||
8,
|
||||
null,
|
||||
]);
|
||||
|
||||
expect(
|
||||
balanceByes(
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
16,
|
||||
),
|
||||
).toEqual([
|
||||
1,
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
3,
|
||||
null,
|
||||
4,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
6,
|
||||
null,
|
||||
7,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const CreateRoundRobinStage = suite("Create a round-robin stage");
|
||||
describe("Create a round-robin stage", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
CreateRoundRobinStage.before.each(() => {
|
||||
storage.reset();
|
||||
});
|
||||
test("should create a round-robin stage", () => {
|
||||
const example = {
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: { groupCount: 2 },
|
||||
} as any;
|
||||
|
||||
CreateRoundRobinStage("should create a round-robin stage", () => {
|
||||
const example = {
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: { groupCount: 2 },
|
||||
} as any;
|
||||
manager.create(example);
|
||||
|
||||
manager.create(example);
|
||||
const stage = storage.select<any>("stage", 0)!;
|
||||
expect(stage.name).toBe(example.name);
|
||||
expect(stage.type).toBe(example.type);
|
||||
|
||||
const stage = storage.select<any>("stage", 0)!;
|
||||
assert.equal(stage.name, example.name);
|
||||
assert.equal(stage.type, example.type);
|
||||
expect(storage.select("group")!.length).toBe(2);
|
||||
expect(storage.select("round")!.length).toBe(6);
|
||||
expect(storage.select("match")!.length).toBe(12);
|
||||
});
|
||||
|
||||
assert.equal(storage.select("group")!.length, 2);
|
||||
assert.equal(storage.select("round")!.length, 6);
|
||||
assert.equal(storage.select("match")!.length, 12);
|
||||
});
|
||||
|
||||
CreateRoundRobinStage(
|
||||
"should create a round-robin stage with a manual seeding",
|
||||
() => {
|
||||
test("should create a round-robin stage with a manual seeding", () => {
|
||||
const example = {
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -60,52 +56,44 @@ CreateRoundRobinStage(
|
||||
matches[0].opponent2.position,
|
||||
];
|
||||
|
||||
assert.equal(participants, example.settings.manualOrdering[groupIndex]);
|
||||
expect(participants).toEqual(example.settings.manualOrdering[groupIndex]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
CreateRoundRobinStage(
|
||||
"should throw if manual ordering has invalid counts",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: {
|
||||
groupCount: 2,
|
||||
manualOrdering: [[1, 4, 6, 7]],
|
||||
},
|
||||
}),
|
||||
test("should throw if manual ordering has invalid counts", () => {
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: {
|
||||
groupCount: 2,
|
||||
manualOrdering: [[1, 4, 6, 7]],
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
"Group count in the manual ordering does not correspond to the given group count.",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: {
|
||||
groupCount: 2,
|
||||
manualOrdering: [
|
||||
[1, 4],
|
||||
[2, 3],
|
||||
],
|
||||
},
|
||||
}),
|
||||
"Not enough seeds in at least one group of the manual ordering.",
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: {
|
||||
groupCount: 2,
|
||||
manualOrdering: [
|
||||
[1, 4],
|
||||
[2, 3],
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toThrow("Not enough seeds in at least one group of the manual ordering.");
|
||||
});
|
||||
|
||||
CreateRoundRobinStage(
|
||||
"should create a round-robin stage without BYE vs. BYE matches",
|
||||
() => {
|
||||
test("should create a round-robin stage without BYE vs. BYE matches", () => {
|
||||
const example = {
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -117,13 +105,10 @@ CreateRoundRobinStage(
|
||||
manager.create(example);
|
||||
|
||||
// One match must be missing.
|
||||
assert.equal(storage.select("match")!.length, 11);
|
||||
},
|
||||
);
|
||||
expect(storage.select("match")!.length).toBe(11);
|
||||
});
|
||||
|
||||
CreateRoundRobinStage(
|
||||
"should create a round-robin stage with to be determined participants",
|
||||
() => {
|
||||
test("should create a round-robin stage with to be determined participants", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -134,15 +119,12 @@ CreateRoundRobinStage(
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(storage.select("group")!.length, 4);
|
||||
assert.equal(storage.select("round")!.length, 4 * 3);
|
||||
assert.equal(storage.select("match")!.length, 4 * 3 * 2);
|
||||
},
|
||||
);
|
||||
expect(storage.select("group")!.length).toBe(4);
|
||||
expect(storage.select("round")!.length).toBe(4 * 3);
|
||||
expect(storage.select("match")!.length).toBe(4 * 3 * 2);
|
||||
});
|
||||
|
||||
CreateRoundRobinStage(
|
||||
"should create a round-robin stage with effort balanced",
|
||||
() => {
|
||||
test("should create a round-robin stage with effort balanced", () => {
|
||||
manager.create({
|
||||
name: "Example with effort balanced",
|
||||
tournamentId: 0,
|
||||
@@ -154,110 +136,96 @@ CreateRoundRobinStage(
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
|
||||
assert.equal(storage.select<any>("match", 0).opponent2.id, 8);
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("match", 0).opponent1.id).toBe(1);
|
||||
expect(storage.select<any>("match", 0).opponent2.id).toBe(8);
|
||||
});
|
||||
|
||||
CreateRoundRobinStage("should throw if no group count given", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
test("should throw if no group count given", () => {
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
}),
|
||||
"You must specify a group count for round-robin stages.",
|
||||
);
|
||||
});
|
||||
).toThrow("You must specify a group count for round-robin stages.");
|
||||
});
|
||||
|
||||
CreateRoundRobinStage(
|
||||
"should throw if the group count is not strictly positive",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
settings: {
|
||||
groupCount: 0,
|
||||
size: 4,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
}),
|
||||
"You must provide a strictly positive group count.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const UpdateRoundRobinScores = suite("Update scores in a round-robin stage");
|
||||
|
||||
UpdateRoundRobinScores.before.each(() => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example scores",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { groupCount: 1 },
|
||||
test("should throw if the group count is not strictly positive", () => {
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
settings: {
|
||||
groupCount: 0,
|
||||
size: 4,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
}),
|
||||
).toThrow("You must provide a strictly positive group count.");
|
||||
});
|
||||
});
|
||||
|
||||
const ExampleUseCase = suite("Example use-case");
|
||||
describe("Update scores in a round-robin stage", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example scores",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { groupCount: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
// Example taken from here:
|
||||
// https://organizer.toornament.com/tournaments/3359823657332629504/stages/3359826493568360448/groups/3359826494507884609/result
|
||||
describe("Example use-case", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example scores",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { groupCount: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
ExampleUseCase.before.each(() => {
|
||||
storage.reset();
|
||||
manager.create({
|
||||
name: "Example scores",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { groupCount: 1 },
|
||||
test("should set all the scores", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" }, // POCEBLO
|
||||
opponent2: { score: 9 }, // AQUELLEHEURE?!
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { score: 3 }, // Ballec Squad
|
||||
opponent2: { score: 16, result: "win" }, // twitch.tv/mrs_fly
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 2,
|
||||
opponent1: { score: 16, result: "win" }, // twitch.tv/mrs_fly
|
||||
opponent2: { score: 0 }, // AQUELLEHEURE?!
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 3,
|
||||
opponent1: { score: 16, result: "win" }, // POCEBLO
|
||||
opponent2: { score: 2 }, // Ballec Squad
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 4,
|
||||
opponent1: { score: 16, result: "win" }, // Ballec Squad
|
||||
opponent2: { score: 12 }, // AQUELLEHEURE?!
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 5,
|
||||
opponent1: { score: 4 }, // twitch.tv/mrs_fly
|
||||
opponent2: { score: 16, result: "win" }, // POCEBLO
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ExampleUseCase("should set all the scores", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" }, // POCEBLO
|
||||
opponent2: { score: 9 }, // AQUELLEHEURE?!
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { score: 3 }, // Ballec Squad
|
||||
opponent2: { score: 16, result: "win" }, // twitch.tv/mrs_fly
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 2,
|
||||
opponent1: { score: 16, result: "win" }, // twitch.tv/mrs_fly
|
||||
opponent2: { score: 0 }, // AQUELLEHEURE?!
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 3,
|
||||
opponent1: { score: 16, result: "win" }, // POCEBLO
|
||||
opponent2: { score: 2 }, // Ballec Squad
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 4,
|
||||
opponent1: { score: 16, result: "win" }, // Ballec Squad
|
||||
opponent2: { score: 12 }, // AQUELLEHEURE?!
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 5,
|
||||
opponent1: { score: 4 }, // twitch.tv/mrs_fly
|
||||
opponent2: { score: 16, result: "win" }, // POCEBLO
|
||||
});
|
||||
});
|
||||
|
||||
CreateRoundRobinStage.run();
|
||||
UpdateRoundRobinScores.run();
|
||||
ExampleUseCase.run();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { Status } from "~/db/types";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
@@ -7,35 +6,32 @@ import { BracketsManager } from "../manager";
|
||||
const storage = new InMemoryDatabase();
|
||||
const manager = new BracketsManager(storage);
|
||||
|
||||
const CreateSingleEliminationStage = suite("Create single elimination stage");
|
||||
describe("Create single elimination stage", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage.before.each(() => {
|
||||
storage.reset();
|
||||
});
|
||||
test("should create a single elimination stage", () => {
|
||||
const example = {
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
} as any;
|
||||
|
||||
CreateSingleEliminationStage("should create a single elimination stage", () => {
|
||||
const example = {
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
} as any;
|
||||
manager.create(example);
|
||||
|
||||
manager.create(example);
|
||||
const stage = storage.select<any>("stage", 0);
|
||||
expect(stage.name).toBe(example.name);
|
||||
expect(stage.type).toBe(example.type);
|
||||
|
||||
const stage = storage.select<any>("stage", 0);
|
||||
assert.equal(stage.name, example.name);
|
||||
assert.equal(stage.type, example.type);
|
||||
expect(storage.select<any>("group")!.length).toBe(1);
|
||||
expect(storage.select<any>("round")!.length).toBe(4);
|
||||
expect(storage.select<any>("match")!.length).toBe(15);
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("group")!.length, 1);
|
||||
assert.equal(storage.select<any>("round")!.length, 4);
|
||||
assert.equal(storage.select<any>("match")!.length, 15);
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should create a single elimination stage with BYEs",
|
||||
() => {
|
||||
test("should create a single elimination stage with BYEs", () => {
|
||||
manager.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
@@ -44,16 +40,13 @@ CreateSingleEliminationStage(
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 4).opponent1.id, 1); // Determined because of opponent's BYE.
|
||||
assert.equal(storage.select<any>("match", 4).opponent2.id, null); // To be determined.
|
||||
assert.equal(storage.select<any>("match", 5).opponent1, null); // BYE propagated.
|
||||
assert.equal(storage.select<any>("match", 5).opponent2.id, null); // To be determined.
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("match", 4).opponent1.id).toBe(1);
|
||||
expect(storage.select<any>("match", 4).opponent2.id).toBe(null);
|
||||
expect(storage.select<any>("match", 5).opponent1).toBe(null);
|
||||
expect(storage.select<any>("match", 5).opponent2.id).toBe(null);
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should create a single elimination stage with consolation final",
|
||||
() => {
|
||||
test("should create a single elimination stage with consolation final", () => {
|
||||
manager.create({
|
||||
name: "Example with consolation final",
|
||||
tournamentId: 0,
|
||||
@@ -62,15 +55,12 @@ CreateSingleEliminationStage(
|
||||
settings: { consolationFinal: true, seedOrdering: ["natural"] },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("group")!.length, 2);
|
||||
assert.equal(storage.select<any>("round")!.length, 4);
|
||||
assert.equal(storage.select<any>("match")!.length, 8);
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("group")!.length).toBe(2);
|
||||
expect(storage.select<any>("round")!.length).toBe(4);
|
||||
expect(storage.select<any>("match")!.length).toBe(8);
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should create a single elimination stage with consolation final and BYEs",
|
||||
() => {
|
||||
test("should create a single elimination stage with consolation final and BYEs", () => {
|
||||
manager.create({
|
||||
name: "Example with consolation final and BYEs",
|
||||
tournamentId: 0,
|
||||
@@ -79,18 +69,15 @@ CreateSingleEliminationStage(
|
||||
settings: { consolationFinal: true, seedOrdering: ["natural"] },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 4).opponent1, null);
|
||||
assert.equal(storage.select<any>("match", 4).opponent2.id, 4);
|
||||
expect(storage.select<any>("match", 4).opponent1).toBe(null);
|
||||
expect(storage.select<any>("match", 4).opponent2.id).toBe(4);
|
||||
|
||||
// Consolation final
|
||||
assert.equal(storage.select<any>("match", 7).opponent1, null);
|
||||
assert.equal(storage.select<any>("match", 7).opponent2.id, null);
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("match", 7).opponent1).toBe(null);
|
||||
expect(storage.select<any>("match", 7).opponent2.id).toBe(null);
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should create a single elimination stage with Bo3 matches",
|
||||
() => {
|
||||
test("should create a single elimination stage with Bo3 matches", () => {
|
||||
manager.create({
|
||||
name: "Example with Bo3 matches",
|
||||
tournamentId: 0,
|
||||
@@ -99,15 +86,12 @@ CreateSingleEliminationStage(
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("group")!.length, 1);
|
||||
assert.equal(storage.select<any>("round")!.length, 3);
|
||||
assert.equal(storage.select<any>("match")!.length, 7);
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("group")!.length).toBe(1);
|
||||
expect(storage.select<any>("round")!.length).toBe(3);
|
||||
expect(storage.select<any>("match")!.length).toBe(7);
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should determine the number property of created stages",
|
||||
() => {
|
||||
test("should determine the number property of created stages", () => {
|
||||
manager.create({
|
||||
name: "Stage 1",
|
||||
tournamentId: 0,
|
||||
@@ -115,7 +99,7 @@ CreateSingleEliminationStage(
|
||||
settings: { size: 2 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("stage", 0).number, 1);
|
||||
expect(storage.select<any>("stage", 0).number).toBe(1);
|
||||
|
||||
manager.create({
|
||||
name: "Stage 2",
|
||||
@@ -124,7 +108,7 @@ CreateSingleEliminationStage(
|
||||
settings: { size: 2 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("stage", 1).number, 2);
|
||||
expect(storage.select<any>("stage", 1).number).toBe(2);
|
||||
|
||||
manager.delete.stage(0);
|
||||
|
||||
@@ -135,13 +119,10 @@ CreateSingleEliminationStage(
|
||||
settings: { size: 2 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("stage", 2).number, 3);
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("stage", 2).number).toBe(3);
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should create a stage with the given number property",
|
||||
() => {
|
||||
test("should create a stage with the given number property", () => {
|
||||
manager.create({
|
||||
name: "Stage 1",
|
||||
tournamentId: 0,
|
||||
@@ -166,13 +147,10 @@ CreateSingleEliminationStage(
|
||||
settings: { size: 2 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("stage", 2).number, 1);
|
||||
},
|
||||
);
|
||||
expect(storage.select<any>("stage", 2).number).toBe(1);
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should throw if the given number property already exists",
|
||||
() => {
|
||||
test("should throw if the given number property already exists", () => {
|
||||
manager.create({
|
||||
name: "Stage 1",
|
||||
tournamentId: 0,
|
||||
@@ -181,44 +159,34 @@ CreateSingleEliminationStage(
|
||||
settings: { size: 2 },
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Stage 1",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
number: 1, // Duplicate
|
||||
settings: { size: 2 },
|
||||
}),
|
||||
"The given stage number already exists.",
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Stage 1",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
number: 1, // Duplicate
|
||||
settings: { size: 2 },
|
||||
}),
|
||||
).toThrow("The given stage number already exists.");
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should throw if the seeding has duplicate participants",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [
|
||||
1,
|
||||
1, // Duplicate
|
||||
3,
|
||||
4,
|
||||
],
|
||||
}),
|
||||
"The seeding has a duplicate participant.",
|
||||
);
|
||||
},
|
||||
);
|
||||
test("should throw if the seeding has duplicate participants", () => {
|
||||
expect(() =>
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [
|
||||
1,
|
||||
1, // Duplicate
|
||||
3,
|
||||
4,
|
||||
],
|
||||
}),
|
||||
).toThrow("The seeding has a duplicate participant.");
|
||||
});
|
||||
|
||||
CreateSingleEliminationStage(
|
||||
"should throw if trying to set a draw as a result",
|
||||
() => {
|
||||
test("should throw if trying to set a draw as a result", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -226,26 +194,21 @@ CreateSingleEliminationStage(
|
||||
seeding: [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "draw" },
|
||||
}),
|
||||
"Having a draw is forbidden in an elimination tournament.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const PreviousAndNextMatchUpdate = suite("Previous and next match update");
|
||||
|
||||
PreviousAndNextMatchUpdate.before.each(() => {
|
||||
storage.reset();
|
||||
expect(() =>
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "draw" },
|
||||
}),
|
||||
).toThrow("Having a draw is forbidden in an elimination tournament.");
|
||||
});
|
||||
});
|
||||
|
||||
PreviousAndNextMatchUpdate(
|
||||
"should determine matches in consolation final",
|
||||
() => {
|
||||
describe("Previous and next match update", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
});
|
||||
|
||||
test("should determine matches in consolation final", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -266,24 +229,17 @@ PreviousAndNextMatchUpdate(
|
||||
opponent2: { score: 16, result: "win" },
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
storage.select<any>("match", 3).opponent1.id, // Determined opponent for the consolation final
|
||||
storage.select<any>("match", 0).opponent2.id, // Loser of Semi 1
|
||||
expect(storage.select<any>("match", 3).opponent1.id).toBe(
|
||||
storage.select<any>("match", 0).opponent2.id,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
storage.select<any>("match", 3).opponent2.id, // Determined opponent for the consolation final
|
||||
storage.select<any>("match", 1).opponent1.id, // Loser of Semi 2
|
||||
expect(storage.select<any>("match", 3).opponent2.id).toBe(
|
||||
storage.select<any>("match", 1).opponent1.id,
|
||||
);
|
||||
expect(storage.select<any>("match", 2).status).toBe(Status.Ready);
|
||||
expect(storage.select<any>("match", 3).status).toBe(Status.Ready);
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 2).status, Status.Ready);
|
||||
assert.equal(storage.select<any>("match", 3).status, Status.Ready);
|
||||
},
|
||||
);
|
||||
|
||||
PreviousAndNextMatchUpdate(
|
||||
"should play both the final and consolation final in parallel",
|
||||
() => {
|
||||
test("should play both the final and consolation final in parallel", () => {
|
||||
manager.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
@@ -310,8 +266,8 @@ PreviousAndNextMatchUpdate(
|
||||
opponent2: { score: 9 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 2).status, Status.Running);
|
||||
assert.equal(storage.select<any>("match", 3).status, Status.Ready);
|
||||
expect(storage.select<any>("match", 2).status).toBe(Status.Running);
|
||||
expect(storage.select<any>("match", 3).status).toBe(Status.Ready);
|
||||
|
||||
manager.update.match({
|
||||
id: 3, // Consolation final
|
||||
@@ -319,8 +275,8 @@ PreviousAndNextMatchUpdate(
|
||||
opponent2: { score: 9 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 2).status, Status.Running);
|
||||
assert.equal(storage.select<any>("match", 3).status, Status.Running);
|
||||
expect(storage.select<any>("match", 2).status).toBe(Status.Running);
|
||||
expect(storage.select<any>("match", 3).status).toBe(Status.Running);
|
||||
|
||||
manager.update.match({
|
||||
id: 3, // Consolation final
|
||||
@@ -328,15 +284,12 @@ PreviousAndNextMatchUpdate(
|
||||
opponent2: { score: 9 },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 2).status, Status.Running);
|
||||
expect(storage.select<any>("match", 2).status).toBe(Status.Running);
|
||||
|
||||
manager.update.match({
|
||||
id: 2, // Final
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 9 },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
CreateSingleEliminationStage.run();
|
||||
PreviousAndNextMatchUpdate.run();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { Status } from "~/db/types";
|
||||
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
|
||||
import { BracketsManager } from "../manager";
|
||||
@@ -15,30 +14,27 @@ const example = {
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
} as any;
|
||||
|
||||
const UpdateMatches = suite("Update matches");
|
||||
|
||||
UpdateMatches.before.each(() => {
|
||||
storage.reset();
|
||||
manager.create(example);
|
||||
});
|
||||
|
||||
UpdateMatches("should start a match", () => {
|
||||
const before = storage.select<any>("match", 0);
|
||||
assert.equal(before.status, Status.Ready);
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 0 },
|
||||
opponent2: { score: 0 },
|
||||
describe("Update matches", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
manager.create(example);
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.status, Status.Running);
|
||||
});
|
||||
test("should start a match", () => {
|
||||
const before = storage.select<any>("match", 0);
|
||||
expect(before.status).toBe(Status.Ready);
|
||||
|
||||
UpdateMatches(
|
||||
"should update the scores for a match and set it to running",
|
||||
() => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 0 },
|
||||
opponent2: { score: 0 },
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
expect(after.status).toBe(Status.Running);
|
||||
});
|
||||
|
||||
test("should update the scores for a match and set it to running", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 2 },
|
||||
@@ -46,38 +42,35 @@ UpdateMatches(
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.status, Status.Running);
|
||||
assert.equal(after.opponent1.score, 2);
|
||||
expect(after.status).toBe(Status.Running);
|
||||
expect(after.opponent1.score).toBe(2);
|
||||
|
||||
// Name should stay. It shouldn't be overwritten.
|
||||
assert.equal(after.opponent1.id, 1);
|
||||
},
|
||||
);
|
||||
|
||||
UpdateMatches("should end the match by only setting the winner", () => {
|
||||
const before = storage.select<any>("match", 0);
|
||||
assert.not.ok(before.opponent1.result);
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
expect(after.opponent1.id).toBe(1);
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.status, Status.Completed);
|
||||
assert.equal(after.opponent1.result, "win");
|
||||
assert.equal(after.opponent2.result, "loss");
|
||||
});
|
||||
test("should end the match by only setting the winner", () => {
|
||||
const before = storage.select<any>("match", 0);
|
||||
expect(before.opponent1.result).toBeFalsy();
|
||||
|
||||
UpdateMatches(
|
||||
"should change the winner of the match and update in the next match",
|
||||
() => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 8).opponent1.id, 1);
|
||||
const after = storage.select<any>("match", 0);
|
||||
expect(after.status).toBe(Status.Completed);
|
||||
expect(after.opponent1.result).toBe("win");
|
||||
expect(after.opponent2.result).toBe("loss");
|
||||
});
|
||||
|
||||
test("should change the winner of the match and update in the next match", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
});
|
||||
|
||||
expect(storage.select<any>("match", 8).opponent1.id).toBe(1);
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
@@ -85,97 +78,94 @@ UpdateMatches(
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.status, Status.Completed);
|
||||
assert.equal(after.opponent1.result, "loss");
|
||||
assert.equal(after.opponent2.result, "win");
|
||||
expect(after.status).toBe(Status.Completed);
|
||||
expect(after.opponent1.result).toBe("loss");
|
||||
expect(after.opponent2.result).toBe("win");
|
||||
|
||||
const nextMatch = storage.select<any>("match", 8);
|
||||
assert.equal(nextMatch.status, Status.Waiting);
|
||||
assert.equal(nextMatch.opponent1.id, 2);
|
||||
},
|
||||
);
|
||||
|
||||
UpdateMatches("should update the status of the next match", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
expect(nextMatch.status).toBe(Status.Waiting);
|
||||
expect(nextMatch.opponent1.id).toBe(2);
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 8).status, Status.Waiting);
|
||||
test("should update the status of the next match", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { result: "win" },
|
||||
expect(storage.select<any>("match", 8).status).toBe(Status.Waiting);
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { result: "win" },
|
||||
});
|
||||
|
||||
expect(storage.select<any>("match", 8).status).toBe(Status.Ready);
|
||||
});
|
||||
|
||||
assert.equal(storage.select<any>("match", 8).status, Status.Ready);
|
||||
});
|
||||
test("should end the match by setting winner and loser", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
status: Status.Running,
|
||||
});
|
||||
|
||||
UpdateMatches("should end the match by setting winner and loser", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
status: Status.Running,
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
opponent2: { result: "loss" },
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
expect(after.status).toBe(Status.Completed);
|
||||
expect(after.opponent1.result).toBe("win");
|
||||
expect(after.opponent2.result).toBe("loss");
|
||||
});
|
||||
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
opponent2: { result: "loss" },
|
||||
test("should remove results from a match without score", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
opponent2: { result: "loss" },
|
||||
});
|
||||
|
||||
manager.reset.matchResults(0);
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
expect(after.status).toBe(Status.Ready);
|
||||
expect(after.opponent1.result).toBeFalsy();
|
||||
expect(after.opponent2.result).toBeFalsy();
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.status, Status.Completed);
|
||||
assert.equal(after.opponent1.result, "win");
|
||||
assert.equal(after.opponent2.result, "loss");
|
||||
});
|
||||
test("should remove results from a match with score", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12, result: "loss" },
|
||||
});
|
||||
|
||||
UpdateMatches("should remove results from a match without score", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { result: "win" },
|
||||
opponent2: { result: "loss" },
|
||||
manager.reset.matchResults(0);
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
expect(after.status).toBe(Status.Running);
|
||||
expect(after.opponent1.result).toBeFalsy();
|
||||
expect(after.opponent2.result).toBeFalsy();
|
||||
});
|
||||
|
||||
manager.reset.matchResults(0);
|
||||
test("should not set the other score to 0 if only one given", () => {
|
||||
// It shouldn't be our decision to set the other score to 0.
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.status, Status.Ready);
|
||||
assert.not.ok(after.opponent1.result);
|
||||
assert.not.ok(after.opponent2.result);
|
||||
});
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { score: 1 },
|
||||
});
|
||||
|
||||
UpdateMatches("should remove results from a match with score", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: { score: 16, result: "win" },
|
||||
opponent2: { score: 12, result: "loss" },
|
||||
const after = storage.select<any>("match", 1);
|
||||
expect(after.status).toBe(Status.Running);
|
||||
expect(after.opponent1.score).toBe(1);
|
||||
expect(after.opponent2.score).toBeFalsy();
|
||||
});
|
||||
|
||||
manager.reset.matchResults(0);
|
||||
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.status, Status.Running);
|
||||
assert.not.ok(after.opponent1.result);
|
||||
assert.not.ok(after.opponent2.result);
|
||||
});
|
||||
|
||||
UpdateMatches("should not set the other score to 0 if only one given", () => {
|
||||
// It shouldn't be our decision to set the other score to 0.
|
||||
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { score: 1 },
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 1);
|
||||
assert.equal(after.status, Status.Running);
|
||||
assert.equal(after.opponent1.score, 1);
|
||||
assert.not.ok(after.opponent2.score);
|
||||
});
|
||||
|
||||
UpdateMatches(
|
||||
"should end the match by setting the winner and the scores",
|
||||
() => {
|
||||
test("should end the match by setting the winner and the scores", () => {
|
||||
manager.update.match({
|
||||
id: 1,
|
||||
opponent1: { score: 6 },
|
||||
@@ -183,74 +173,67 @@ UpdateMatches(
|
||||
});
|
||||
|
||||
const after = storage.select<any>("match", 1);
|
||||
assert.equal(after.status, Status.Completed);
|
||||
expect(after.status).toBe(Status.Completed);
|
||||
|
||||
assert.equal(after.opponent1.result, "loss");
|
||||
assert.equal(after.opponent1.score, 6);
|
||||
expect(after.opponent1.result).toBe("loss");
|
||||
expect(after.opponent1.score).toBe(6);
|
||||
|
||||
assert.equal(after.opponent2.result, "win");
|
||||
assert.equal(after.opponent2.score, 3);
|
||||
},
|
||||
);
|
||||
expect(after.opponent2.result).toBe("win");
|
||||
expect(after.opponent2.score).toBe(3);
|
||||
});
|
||||
|
||||
UpdateMatches("should throw if two winners", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
test("should throw if two winners", () => {
|
||||
expect(() =>
|
||||
manager.update.match({
|
||||
id: 3,
|
||||
opponent1: { result: "win" },
|
||||
opponent2: { result: "win" },
|
||||
}),
|
||||
"There are two winners.",
|
||||
);
|
||||
).toThrow("There are two winners.");
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
expect(() =>
|
||||
manager.update.match({
|
||||
id: 3,
|
||||
opponent1: { result: "loss" },
|
||||
opponent2: { result: "loss" },
|
||||
}),
|
||||
"There are two losers.",
|
||||
);
|
||||
});
|
||||
|
||||
const GiveOpponentIds = suite("Give opponent IDs when updating");
|
||||
|
||||
GiveOpponentIds.before.each(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
).toThrow("There are two losers.");
|
||||
});
|
||||
});
|
||||
|
||||
GiveOpponentIds("should update the right opponents based on their IDs", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: {
|
||||
id: 2,
|
||||
score: 10,
|
||||
},
|
||||
opponent2: {
|
||||
id: 1,
|
||||
score: 5,
|
||||
},
|
||||
describe("Give opponent IDs when updating", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
|
||||
manager.create({
|
||||
name: "Amateur",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
});
|
||||
});
|
||||
|
||||
// Actual results must be inverted.
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.equal(after.opponent1.score, 5);
|
||||
assert.equal(after.opponent2.score, 10);
|
||||
});
|
||||
test("should update the right opponents based on their IDs", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: {
|
||||
id: 2,
|
||||
score: 10,
|
||||
},
|
||||
opponent2: {
|
||||
id: 1,
|
||||
score: 5,
|
||||
},
|
||||
});
|
||||
|
||||
GiveOpponentIds(
|
||||
"should update the right opponent based on its ID, the other one is the remaining one",
|
||||
() => {
|
||||
// Actual results must be inverted.
|
||||
const after = storage.select<any>("match", 0);
|
||||
expect(after.opponent1.score).toBe(5);
|
||||
expect(after.opponent2.score).toBe(10);
|
||||
});
|
||||
|
||||
test("should update the right opponent based on its ID, the other one is the remaining one", () => {
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: {
|
||||
@@ -261,58 +244,42 @@ GiveOpponentIds(
|
||||
|
||||
// Actual results must be inverted.
|
||||
const after = storage.select<any>("match", 0);
|
||||
assert.not.ok(after.opponent1.score);
|
||||
assert.equal(after.opponent2.score, 10);
|
||||
},
|
||||
);
|
||||
expect(after.opponent1.score).toBeFalsy();
|
||||
expect(after.opponent2.score).toBe(10);
|
||||
});
|
||||
|
||||
GiveOpponentIds(
|
||||
"should throw when the given opponent ID does not exist in the match",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: {
|
||||
id: 3, // Belongs to match id 1.
|
||||
score: 10,
|
||||
},
|
||||
}),
|
||||
/The given opponent[12] ID does not exist in this match./,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const LockedMatches = suite("Locked matches");
|
||||
|
||||
LockedMatches.before.each(() => {
|
||||
storage.reset();
|
||||
manager.create(example);
|
||||
test("should throw when the given opponent ID does not exist in the match", () => {
|
||||
expect(() =>
|
||||
manager.update.match({
|
||||
id: 0,
|
||||
opponent1: {
|
||||
id: 3, // Belongs to match id 1.
|
||||
score: 10,
|
||||
},
|
||||
}),
|
||||
).toThrow(/The given opponent[12] ID does not exist in this match./);
|
||||
});
|
||||
});
|
||||
|
||||
LockedMatches(
|
||||
"should throw when the matches leading to the match have not been completed yet",
|
||||
() => {
|
||||
describe("Locked matches", () => {
|
||||
beforeEach(() => {
|
||||
storage.reset();
|
||||
manager.create(example);
|
||||
});
|
||||
|
||||
test("should throw when the matches leading to the match have not been completed yet", () => {
|
||||
manager.update.match({ id: 0 }); // No problem when no previous match.
|
||||
assert.throws(
|
||||
() => manager.update.match({ id: 8 }),
|
||||
expect(() => manager.update.match({ id: 8 })).toThrow(
|
||||
"The match is locked.",
|
||||
); // First match of WB Round 2.
|
||||
assert.throws(
|
||||
() => manager.update.match({ id: 15 }),
|
||||
expect(() => manager.update.match({ id: 15 })).toThrow(
|
||||
"The match is locked.",
|
||||
); // First match of LB Round 1.
|
||||
assert.throws(
|
||||
() => manager.update.match({ id: 19 }),
|
||||
expect(() => manager.update.match({ id: 19 })).toThrow(
|
||||
"The match is locked.",
|
||||
); // First match of LB Round 1.
|
||||
assert.throws(
|
||||
() => manager.update.match({ id: 23 }),
|
||||
expect(() => manager.update.match({ id: 23 })).toThrow(
|
||||
"The match is locked.",
|
||||
); // First match of LB Round 3.
|
||||
},
|
||||
);
|
||||
|
||||
UpdateMatches.run();
|
||||
GiveOpponentIds.run();
|
||||
LockedMatches.run();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { resolve } from "node:path";
|
||||
import { createCookie } from "@remix-run/node";
|
||||
import Backend from "i18next-fs-backend";
|
||||
import { RemixI18Next } from "remix-i18next/server";
|
||||
import { config } from "./config";
|
||||
import { resources } from "./resources.server";
|
||||
|
||||
const TEN_YEARS_IN_SECONDS = 31_536_000 * 10;
|
||||
|
||||
@@ -20,11 +19,8 @@ export const i18next = new RemixI18Next({
|
||||
},
|
||||
i18next: {
|
||||
...config,
|
||||
backend: {
|
||||
loadPath: resolve("./locales/{{lng}}/{{ns}}.json"),
|
||||
},
|
||||
resources: resources,
|
||||
},
|
||||
backend: Backend,
|
||||
});
|
||||
|
||||
export default i18next;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { resolve } from "node:path";
|
||||
import type { EntryContext } from "@remix-run/server-runtime";
|
||||
import { createInstance } from "i18next";
|
||||
import type { i18n as i18nType } from "i18next";
|
||||
import Backend from "i18next-fs-backend";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { config } from "./config";
|
||||
import i18next from "./i18next.server";
|
||||
|
||||
export async function i18Instance(request: Request, context: EntryContext) {
|
||||
const instance = createInstance() as i18nType;
|
||||
|
||||
const lng = await i18next.getLocale(request);
|
||||
const ns = i18next.getRouteNamespaces(context);
|
||||
|
||||
await instance
|
||||
.use(initReactI18next)
|
||||
.use(Backend)
|
||||
.init({
|
||||
...config,
|
||||
lng,
|
||||
ns,
|
||||
backend: {
|
||||
loadPath: resolve("./locales/{{lng}}/{{ns}}.json"),
|
||||
},
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
615
app/modules/i18n/resources.server.ts
Normal file
615
app/modules/i18n/resources.server.ts
Normal file
@@ -0,0 +1,615 @@
|
||||
// This file is generated by scripts/generate-resources-file.ts
|
||||
|
||||
import analyzerDa from "../../../locales/da/analyzer.json";
|
||||
import artDa from "../../../locales/da/art.json";
|
||||
import badgesDa from "../../../locales/da/badges.json";
|
||||
import buildsDa from "../../../locales/da/builds.json";
|
||||
import calendarDa from "../../../locales/da/calendar.json";
|
||||
import commonDa from "../../../locales/da/common.json";
|
||||
import contributionsDa from "../../../locales/da/contributions.json";
|
||||
import faqDa from "../../../locales/da/faq.json";
|
||||
import gameMiscDa from "../../../locales/da/game-misc.json";
|
||||
import gearDa from "../../../locales/da/gear.json";
|
||||
import lfgDa from "../../../locales/da/lfg.json";
|
||||
import orgDa from "../../../locales/da/org.json";
|
||||
import qDa from "../../../locales/da/q.json";
|
||||
import teamDa from "../../../locales/da/team.json";
|
||||
import tournamentDa from "../../../locales/da/tournament.json";
|
||||
import userDa from "../../../locales/da/user.json";
|
||||
import vodsDa from "../../../locales/da/vods.json";
|
||||
import weaponsDa from "../../../locales/da/weapons.json";
|
||||
import analyzerDe from "../../../locales/de/analyzer.json";
|
||||
import artDe from "../../../locales/de/art.json";
|
||||
import badgesDe from "../../../locales/de/badges.json";
|
||||
import buildsDe from "../../../locales/de/builds.json";
|
||||
import calendarDe from "../../../locales/de/calendar.json";
|
||||
import commonDe from "../../../locales/de/common.json";
|
||||
import contributionsDe from "../../../locales/de/contributions.json";
|
||||
import faqDe from "../../../locales/de/faq.json";
|
||||
import gameMiscDe from "../../../locales/de/game-misc.json";
|
||||
import gearDe from "../../../locales/de/gear.json";
|
||||
import lfgDe from "../../../locales/de/lfg.json";
|
||||
import orgDe from "../../../locales/de/org.json";
|
||||
import qDe from "../../../locales/de/q.json";
|
||||
import teamDe from "../../../locales/de/team.json";
|
||||
import tournamentDe from "../../../locales/de/tournament.json";
|
||||
import userDe from "../../../locales/de/user.json";
|
||||
import vodsDe from "../../../locales/de/vods.json";
|
||||
import weaponsDe from "../../../locales/de/weapons.json";
|
||||
import analyzer from "../../../locales/en/analyzer.json";
|
||||
import art from "../../../locales/en/art.json";
|
||||
import badges from "../../../locales/en/badges.json";
|
||||
import builds from "../../../locales/en/builds.json";
|
||||
import calendar from "../../../locales/en/calendar.json";
|
||||
import common from "../../../locales/en/common.json";
|
||||
import contributions from "../../../locales/en/contributions.json";
|
||||
import faq from "../../../locales/en/faq.json";
|
||||
import gameMisc from "../../../locales/en/game-misc.json";
|
||||
import gear from "../../../locales/en/gear.json";
|
||||
import lfg from "../../../locales/en/lfg.json";
|
||||
import org from "../../../locales/en/org.json";
|
||||
import q from "../../../locales/en/q.json";
|
||||
import team from "../../../locales/en/team.json";
|
||||
import tournament from "../../../locales/en/tournament.json";
|
||||
import user from "../../../locales/en/user.json";
|
||||
import vods from "../../../locales/en/vods.json";
|
||||
import weapons from "../../../locales/en/weapons.json";
|
||||
import analyzerEsEs from "../../../locales/es-ES/analyzer.json";
|
||||
import artEsEs from "../../../locales/es-ES/art.json";
|
||||
import badgesEsEs from "../../../locales/es-ES/badges.json";
|
||||
import buildsEsEs from "../../../locales/es-ES/builds.json";
|
||||
import calendarEsEs from "../../../locales/es-ES/calendar.json";
|
||||
import commonEsEs from "../../../locales/es-ES/common.json";
|
||||
import contributionsEsEs from "../../../locales/es-ES/contributions.json";
|
||||
import faqEsEs from "../../../locales/es-ES/faq.json";
|
||||
import gameMiscEsEs from "../../../locales/es-ES/game-misc.json";
|
||||
import gearEsEs from "../../../locales/es-ES/gear.json";
|
||||
import lfgEsEs from "../../../locales/es-ES/lfg.json";
|
||||
import orgEsEs from "../../../locales/es-ES/org.json";
|
||||
import qEsEs from "../../../locales/es-ES/q.json";
|
||||
import teamEsEs from "../../../locales/es-ES/team.json";
|
||||
import tournamentEsEs from "../../../locales/es-ES/tournament.json";
|
||||
import userEsEs from "../../../locales/es-ES/user.json";
|
||||
import vodsEsEs from "../../../locales/es-ES/vods.json";
|
||||
import weaponsEsEs from "../../../locales/es-ES/weapons.json";
|
||||
import analyzerEsUs from "../../../locales/es-US/analyzer.json";
|
||||
import artEsUs from "../../../locales/es-US/art.json";
|
||||
import badgesEsUs from "../../../locales/es-US/badges.json";
|
||||
import buildsEsUs from "../../../locales/es-US/builds.json";
|
||||
import calendarEsUs from "../../../locales/es-US/calendar.json";
|
||||
import commonEsUs from "../../../locales/es-US/common.json";
|
||||
import contributionsEsUs from "../../../locales/es-US/contributions.json";
|
||||
import faqEsUs from "../../../locales/es-US/faq.json";
|
||||
import gameMiscEsUs from "../../../locales/es-US/game-misc.json";
|
||||
import gearEsUs from "../../../locales/es-US/gear.json";
|
||||
import lfgEsUs from "../../../locales/es-US/lfg.json";
|
||||
import orgEsUs from "../../../locales/es-US/org.json";
|
||||
import qEsUs from "../../../locales/es-US/q.json";
|
||||
import teamEsUs from "../../../locales/es-US/team.json";
|
||||
import tournamentEsUs from "../../../locales/es-US/tournament.json";
|
||||
import userEsUs from "../../../locales/es-US/user.json";
|
||||
import vodsEsUs from "../../../locales/es-US/vods.json";
|
||||
import weaponsEsUs from "../../../locales/es-US/weapons.json";
|
||||
import analyzerFrCa from "../../../locales/fr-CA/analyzer.json";
|
||||
import artFrCa from "../../../locales/fr-CA/art.json";
|
||||
import badgesFrCa from "../../../locales/fr-CA/badges.json";
|
||||
import buildsFrCa from "../../../locales/fr-CA/builds.json";
|
||||
import calendarFrCa from "../../../locales/fr-CA/calendar.json";
|
||||
import commonFrCa from "../../../locales/fr-CA/common.json";
|
||||
import contributionsFrCa from "../../../locales/fr-CA/contributions.json";
|
||||
import faqFrCa from "../../../locales/fr-CA/faq.json";
|
||||
import gameMiscFrCa from "../../../locales/fr-CA/game-misc.json";
|
||||
import gearFrCa from "../../../locales/fr-CA/gear.json";
|
||||
import lfgFrCa from "../../../locales/fr-CA/lfg.json";
|
||||
import orgFrCa from "../../../locales/fr-CA/org.json";
|
||||
import qFrCa from "../../../locales/fr-CA/q.json";
|
||||
import teamFrCa from "../../../locales/fr-CA/team.json";
|
||||
import tournamentFrCa from "../../../locales/fr-CA/tournament.json";
|
||||
import userFrCa from "../../../locales/fr-CA/user.json";
|
||||
import vodsFrCa from "../../../locales/fr-CA/vods.json";
|
||||
import weaponsFrCa from "../../../locales/fr-CA/weapons.json";
|
||||
import analyzerFrEu from "../../../locales/fr-EU/analyzer.json";
|
||||
import artFrEu from "../../../locales/fr-EU/art.json";
|
||||
import badgesFrEu from "../../../locales/fr-EU/badges.json";
|
||||
import buildsFrEu from "../../../locales/fr-EU/builds.json";
|
||||
import calendarFrEu from "../../../locales/fr-EU/calendar.json";
|
||||
import commonFrEu from "../../../locales/fr-EU/common.json";
|
||||
import contributionsFrEu from "../../../locales/fr-EU/contributions.json";
|
||||
import faqFrEu from "../../../locales/fr-EU/faq.json";
|
||||
import gameMiscFrEu from "../../../locales/fr-EU/game-misc.json";
|
||||
import gearFrEu from "../../../locales/fr-EU/gear.json";
|
||||
import lfgFrEu from "../../../locales/fr-EU/lfg.json";
|
||||
import orgFrEu from "../../../locales/fr-EU/org.json";
|
||||
import qFrEu from "../../../locales/fr-EU/q.json";
|
||||
import teamFrEu from "../../../locales/fr-EU/team.json";
|
||||
import tournamentFrEu from "../../../locales/fr-EU/tournament.json";
|
||||
import userFrEu from "../../../locales/fr-EU/user.json";
|
||||
import vodsFrEu from "../../../locales/fr-EU/vods.json";
|
||||
import weaponsFrEu from "../../../locales/fr-EU/weapons.json";
|
||||
import analyzerHe from "../../../locales/he/analyzer.json";
|
||||
import artHe from "../../../locales/he/art.json";
|
||||
import badgesHe from "../../../locales/he/badges.json";
|
||||
import buildsHe from "../../../locales/he/builds.json";
|
||||
import calendarHe from "../../../locales/he/calendar.json";
|
||||
import commonHe from "../../../locales/he/common.json";
|
||||
import contributionsHe from "../../../locales/he/contributions.json";
|
||||
import faqHe from "../../../locales/he/faq.json";
|
||||
import gameMiscHe from "../../../locales/he/game-misc.json";
|
||||
import gearHe from "../../../locales/he/gear.json";
|
||||
import lfgHe from "../../../locales/he/lfg.json";
|
||||
import orgHe from "../../../locales/he/org.json";
|
||||
import qHe from "../../../locales/he/q.json";
|
||||
import teamHe from "../../../locales/he/team.json";
|
||||
import tournamentHe from "../../../locales/he/tournament.json";
|
||||
import userHe from "../../../locales/he/user.json";
|
||||
import vodsHe from "../../../locales/he/vods.json";
|
||||
import weaponsHe from "../../../locales/he/weapons.json";
|
||||
import analyzerIt from "../../../locales/it/analyzer.json";
|
||||
import artIt from "../../../locales/it/art.json";
|
||||
import badgesIt from "../../../locales/it/badges.json";
|
||||
import buildsIt from "../../../locales/it/builds.json";
|
||||
import calendarIt from "../../../locales/it/calendar.json";
|
||||
import commonIt from "../../../locales/it/common.json";
|
||||
import contributionsIt from "../../../locales/it/contributions.json";
|
||||
import faqIt from "../../../locales/it/faq.json";
|
||||
import gameMiscIt from "../../../locales/it/game-misc.json";
|
||||
import gearIt from "../../../locales/it/gear.json";
|
||||
import lfgIt from "../../../locales/it/lfg.json";
|
||||
import orgIt from "../../../locales/it/org.json";
|
||||
import qIt from "../../../locales/it/q.json";
|
||||
import teamIt from "../../../locales/it/team.json";
|
||||
import tournamentIt from "../../../locales/it/tournament.json";
|
||||
import userIt from "../../../locales/it/user.json";
|
||||
import vodsIt from "../../../locales/it/vods.json";
|
||||
import weaponsIt from "../../../locales/it/weapons.json";
|
||||
import analyzerJa from "../../../locales/ja/analyzer.json";
|
||||
import artJa from "../../../locales/ja/art.json";
|
||||
import badgesJa from "../../../locales/ja/badges.json";
|
||||
import buildsJa from "../../../locales/ja/builds.json";
|
||||
import calendarJa from "../../../locales/ja/calendar.json";
|
||||
import commonJa from "../../../locales/ja/common.json";
|
||||
import contributionsJa from "../../../locales/ja/contributions.json";
|
||||
import faqJa from "../../../locales/ja/faq.json";
|
||||
import gameMiscJa from "../../../locales/ja/game-misc.json";
|
||||
import gearJa from "../../../locales/ja/gear.json";
|
||||
import lfgJa from "../../../locales/ja/lfg.json";
|
||||
import orgJa from "../../../locales/ja/org.json";
|
||||
import qJa from "../../../locales/ja/q.json";
|
||||
import teamJa from "../../../locales/ja/team.json";
|
||||
import tournamentJa from "../../../locales/ja/tournament.json";
|
||||
import userJa from "../../../locales/ja/user.json";
|
||||
import vodsJa from "../../../locales/ja/vods.json";
|
||||
import weaponsJa from "../../../locales/ja/weapons.json";
|
||||
import analyzerKo from "../../../locales/ko/analyzer.json";
|
||||
import artKo from "../../../locales/ko/art.json";
|
||||
import badgesKo from "../../../locales/ko/badges.json";
|
||||
import buildsKo from "../../../locales/ko/builds.json";
|
||||
import calendarKo from "../../../locales/ko/calendar.json";
|
||||
import commonKo from "../../../locales/ko/common.json";
|
||||
import contributionsKo from "../../../locales/ko/contributions.json";
|
||||
import faqKo from "../../../locales/ko/faq.json";
|
||||
import gameMiscKo from "../../../locales/ko/game-misc.json";
|
||||
import gearKo from "../../../locales/ko/gear.json";
|
||||
import lfgKo from "../../../locales/ko/lfg.json";
|
||||
import orgKo from "../../../locales/ko/org.json";
|
||||
import qKo from "../../../locales/ko/q.json";
|
||||
import teamKo from "../../../locales/ko/team.json";
|
||||
import tournamentKo from "../../../locales/ko/tournament.json";
|
||||
import userKo from "../../../locales/ko/user.json";
|
||||
import vodsKo from "../../../locales/ko/vods.json";
|
||||
import weaponsKo from "../../../locales/ko/weapons.json";
|
||||
import analyzerNl from "../../../locales/nl/analyzer.json";
|
||||
import artNl from "../../../locales/nl/art.json";
|
||||
import badgesNl from "../../../locales/nl/badges.json";
|
||||
import buildsNl from "../../../locales/nl/builds.json";
|
||||
import calendarNl from "../../../locales/nl/calendar.json";
|
||||
import commonNl from "../../../locales/nl/common.json";
|
||||
import contributionsNl from "../../../locales/nl/contributions.json";
|
||||
import faqNl from "../../../locales/nl/faq.json";
|
||||
import gameMiscNl from "../../../locales/nl/game-misc.json";
|
||||
import gearNl from "../../../locales/nl/gear.json";
|
||||
import lfgNl from "../../../locales/nl/lfg.json";
|
||||
import orgNl from "../../../locales/nl/org.json";
|
||||
import qNl from "../../../locales/nl/q.json";
|
||||
import teamNl from "../../../locales/nl/team.json";
|
||||
import tournamentNl from "../../../locales/nl/tournament.json";
|
||||
import userNl from "../../../locales/nl/user.json";
|
||||
import vodsNl from "../../../locales/nl/vods.json";
|
||||
import weaponsNl from "../../../locales/nl/weapons.json";
|
||||
import analyzerPl from "../../../locales/pl/analyzer.json";
|
||||
import artPl from "../../../locales/pl/art.json";
|
||||
import badgesPl from "../../../locales/pl/badges.json";
|
||||
import buildsPl from "../../../locales/pl/builds.json";
|
||||
import calendarPl from "../../../locales/pl/calendar.json";
|
||||
import commonPl from "../../../locales/pl/common.json";
|
||||
import contributionsPl from "../../../locales/pl/contributions.json";
|
||||
import faqPl from "../../../locales/pl/faq.json";
|
||||
import gameMiscPl from "../../../locales/pl/game-misc.json";
|
||||
import gearPl from "../../../locales/pl/gear.json";
|
||||
import lfgPl from "../../../locales/pl/lfg.json";
|
||||
import orgPl from "../../../locales/pl/org.json";
|
||||
import qPl from "../../../locales/pl/q.json";
|
||||
import teamPl from "../../../locales/pl/team.json";
|
||||
import tournamentPl from "../../../locales/pl/tournament.json";
|
||||
import userPl from "../../../locales/pl/user.json";
|
||||
import vodsPl from "../../../locales/pl/vods.json";
|
||||
import weaponsPl from "../../../locales/pl/weapons.json";
|
||||
import analyzerPtBr from "../../../locales/pt-BR/analyzer.json";
|
||||
import artPtBr from "../../../locales/pt-BR/art.json";
|
||||
import badgesPtBr from "../../../locales/pt-BR/badges.json";
|
||||
import buildsPtBr from "../../../locales/pt-BR/builds.json";
|
||||
import calendarPtBr from "../../../locales/pt-BR/calendar.json";
|
||||
import commonPtBr from "../../../locales/pt-BR/common.json";
|
||||
import contributionsPtBr from "../../../locales/pt-BR/contributions.json";
|
||||
import faqPtBr from "../../../locales/pt-BR/faq.json";
|
||||
import gameMiscPtBr from "../../../locales/pt-BR/game-misc.json";
|
||||
import gearPtBr from "../../../locales/pt-BR/gear.json";
|
||||
import lfgPtBr from "../../../locales/pt-BR/lfg.json";
|
||||
import orgPtBr from "../../../locales/pt-BR/org.json";
|
||||
import qPtBr from "../../../locales/pt-BR/q.json";
|
||||
import teamPtBr from "../../../locales/pt-BR/team.json";
|
||||
import tournamentPtBr from "../../../locales/pt-BR/tournament.json";
|
||||
import userPtBr from "../../../locales/pt-BR/user.json";
|
||||
import vodsPtBr from "../../../locales/pt-BR/vods.json";
|
||||
import weaponsPtBr from "../../../locales/pt-BR/weapons.json";
|
||||
import analyzerRu from "../../../locales/ru/analyzer.json";
|
||||
import artRu from "../../../locales/ru/art.json";
|
||||
import badgesRu from "../../../locales/ru/badges.json";
|
||||
import buildsRu from "../../../locales/ru/builds.json";
|
||||
import calendarRu from "../../../locales/ru/calendar.json";
|
||||
import commonRu from "../../../locales/ru/common.json";
|
||||
import contributionsRu from "../../../locales/ru/contributions.json";
|
||||
import faqRu from "../../../locales/ru/faq.json";
|
||||
import gameMiscRu from "../../../locales/ru/game-misc.json";
|
||||
import gearRu from "../../../locales/ru/gear.json";
|
||||
import lfgRu from "../../../locales/ru/lfg.json";
|
||||
import orgRu from "../../../locales/ru/org.json";
|
||||
import qRu from "../../../locales/ru/q.json";
|
||||
import teamRu from "../../../locales/ru/team.json";
|
||||
import tournamentRu from "../../../locales/ru/tournament.json";
|
||||
import userRu from "../../../locales/ru/user.json";
|
||||
import vodsRu from "../../../locales/ru/vods.json";
|
||||
import weaponsRu from "../../../locales/ru/weapons.json";
|
||||
import analyzerZh from "../../../locales/zh/analyzer.json";
|
||||
import artZh from "../../../locales/zh/art.json";
|
||||
import badgesZh from "../../../locales/zh/badges.json";
|
||||
import buildsZh from "../../../locales/zh/builds.json";
|
||||
import calendarZh from "../../../locales/zh/calendar.json";
|
||||
import commonZh from "../../../locales/zh/common.json";
|
||||
import contributionsZh from "../../../locales/zh/contributions.json";
|
||||
import faqZh from "../../../locales/zh/faq.json";
|
||||
import gameMiscZh from "../../../locales/zh/game-misc.json";
|
||||
import gearZh from "../../../locales/zh/gear.json";
|
||||
import lfgZh from "../../../locales/zh/lfg.json";
|
||||
import orgZh from "../../../locales/zh/org.json";
|
||||
import qZh from "../../../locales/zh/q.json";
|
||||
import teamZh from "../../../locales/zh/team.json";
|
||||
import tournamentZh from "../../../locales/zh/tournament.json";
|
||||
import userZh from "../../../locales/zh/user.json";
|
||||
import vodsZh from "../../../locales/zh/vods.json";
|
||||
import weaponsZh from "../../../locales/zh/weapons.json";
|
||||
|
||||
export const resources = {
|
||||
"es-US": {
|
||||
gear: gearEsUs,
|
||||
faq: faqEsUs,
|
||||
weapons: weaponsEsUs,
|
||||
common: commonEsUs,
|
||||
"game-misc": gameMiscEsUs,
|
||||
tournament: tournamentEsUs,
|
||||
user: userEsUs,
|
||||
q: qEsUs,
|
||||
art: artEsUs,
|
||||
builds: buildsEsUs,
|
||||
lfg: lfgEsUs,
|
||||
vods: vodsEsUs,
|
||||
calendar: calendarEsUs,
|
||||
org: orgEsUs,
|
||||
badges: badgesEsUs,
|
||||
contributions: contributionsEsUs,
|
||||
team: teamEsUs,
|
||||
analyzer: analyzerEsUs,
|
||||
},
|
||||
en: {
|
||||
gear: gear,
|
||||
faq: faq,
|
||||
weapons: weapons,
|
||||
common: common,
|
||||
"game-misc": gameMisc,
|
||||
tournament: tournament,
|
||||
user: user,
|
||||
q: q,
|
||||
art: art,
|
||||
builds: builds,
|
||||
lfg: lfg,
|
||||
vods: vods,
|
||||
calendar: calendar,
|
||||
org: org,
|
||||
badges: badges,
|
||||
contributions: contributions,
|
||||
team: team,
|
||||
analyzer: analyzer,
|
||||
},
|
||||
ko: {
|
||||
gear: gearKo,
|
||||
faq: faqKo,
|
||||
weapons: weaponsKo,
|
||||
common: commonKo,
|
||||
"game-misc": gameMiscKo,
|
||||
tournament: tournamentKo,
|
||||
user: userKo,
|
||||
q: qKo,
|
||||
art: artKo,
|
||||
builds: buildsKo,
|
||||
lfg: lfgKo,
|
||||
vods: vodsKo,
|
||||
calendar: calendarKo,
|
||||
org: orgKo,
|
||||
badges: badgesKo,
|
||||
contributions: contributionsKo,
|
||||
team: teamKo,
|
||||
analyzer: analyzerKo,
|
||||
},
|
||||
de: {
|
||||
gear: gearDe,
|
||||
faq: faqDe,
|
||||
weapons: weaponsDe,
|
||||
common: commonDe,
|
||||
"game-misc": gameMiscDe,
|
||||
tournament: tournamentDe,
|
||||
user: userDe,
|
||||
q: qDe,
|
||||
art: artDe,
|
||||
builds: buildsDe,
|
||||
lfg: lfgDe,
|
||||
vods: vodsDe,
|
||||
calendar: calendarDe,
|
||||
org: orgDe,
|
||||
badges: badgesDe,
|
||||
contributions: contributionsDe,
|
||||
team: teamDe,
|
||||
analyzer: analyzerDe,
|
||||
},
|
||||
nl: {
|
||||
gear: gearNl,
|
||||
faq: faqNl,
|
||||
weapons: weaponsNl,
|
||||
common: commonNl,
|
||||
"game-misc": gameMiscNl,
|
||||
tournament: tournamentNl,
|
||||
user: userNl,
|
||||
q: qNl,
|
||||
art: artNl,
|
||||
builds: buildsNl,
|
||||
lfg: lfgNl,
|
||||
vods: vodsNl,
|
||||
calendar: calendarNl,
|
||||
org: orgNl,
|
||||
badges: badgesNl,
|
||||
contributions: contributionsNl,
|
||||
team: teamNl,
|
||||
analyzer: analyzerNl,
|
||||
},
|
||||
"pt-BR": {
|
||||
gear: gearPtBr,
|
||||
faq: faqPtBr,
|
||||
weapons: weaponsPtBr,
|
||||
common: commonPtBr,
|
||||
"game-misc": gameMiscPtBr,
|
||||
tournament: tournamentPtBr,
|
||||
user: userPtBr,
|
||||
q: qPtBr,
|
||||
art: artPtBr,
|
||||
builds: buildsPtBr,
|
||||
lfg: lfgPtBr,
|
||||
vods: vodsPtBr,
|
||||
calendar: calendarPtBr,
|
||||
org: orgPtBr,
|
||||
badges: badgesPtBr,
|
||||
contributions: contributionsPtBr,
|
||||
team: teamPtBr,
|
||||
analyzer: analyzerPtBr,
|
||||
},
|
||||
zh: {
|
||||
gear: gearZh,
|
||||
faq: faqZh,
|
||||
weapons: weaponsZh,
|
||||
common: commonZh,
|
||||
"game-misc": gameMiscZh,
|
||||
tournament: tournamentZh,
|
||||
user: userZh,
|
||||
q: qZh,
|
||||
art: artZh,
|
||||
builds: buildsZh,
|
||||
lfg: lfgZh,
|
||||
vods: vodsZh,
|
||||
calendar: calendarZh,
|
||||
org: orgZh,
|
||||
badges: badgesZh,
|
||||
contributions: contributionsZh,
|
||||
team: teamZh,
|
||||
analyzer: analyzerZh,
|
||||
},
|
||||
"fr-CA": {
|
||||
gear: gearFrCa,
|
||||
faq: faqFrCa,
|
||||
weapons: weaponsFrCa,
|
||||
common: commonFrCa,
|
||||
"game-misc": gameMiscFrCa,
|
||||
tournament: tournamentFrCa,
|
||||
user: userFrCa,
|
||||
q: qFrCa,
|
||||
art: artFrCa,
|
||||
builds: buildsFrCa,
|
||||
lfg: lfgFrCa,
|
||||
vods: vodsFrCa,
|
||||
calendar: calendarFrCa,
|
||||
org: orgFrCa,
|
||||
badges: badgesFrCa,
|
||||
contributions: contributionsFrCa,
|
||||
team: teamFrCa,
|
||||
analyzer: analyzerFrCa,
|
||||
},
|
||||
ru: {
|
||||
gear: gearRu,
|
||||
faq: faqRu,
|
||||
weapons: weaponsRu,
|
||||
common: commonRu,
|
||||
"game-misc": gameMiscRu,
|
||||
tournament: tournamentRu,
|
||||
user: userRu,
|
||||
q: qRu,
|
||||
art: artRu,
|
||||
builds: buildsRu,
|
||||
lfg: lfgRu,
|
||||
vods: vodsRu,
|
||||
calendar: calendarRu,
|
||||
org: orgRu,
|
||||
badges: badgesRu,
|
||||
contributions: contributionsRu,
|
||||
team: teamRu,
|
||||
analyzer: analyzerRu,
|
||||
},
|
||||
it: {
|
||||
gear: gearIt,
|
||||
faq: faqIt,
|
||||
weapons: weaponsIt,
|
||||
common: commonIt,
|
||||
"game-misc": gameMiscIt,
|
||||
tournament: tournamentIt,
|
||||
user: userIt,
|
||||
q: qIt,
|
||||
art: artIt,
|
||||
builds: buildsIt,
|
||||
lfg: lfgIt,
|
||||
vods: vodsIt,
|
||||
calendar: calendarIt,
|
||||
org: orgIt,
|
||||
badges: badgesIt,
|
||||
contributions: contributionsIt,
|
||||
team: teamIt,
|
||||
analyzer: analyzerIt,
|
||||
},
|
||||
ja: {
|
||||
gear: gearJa,
|
||||
faq: faqJa,
|
||||
weapons: weaponsJa,
|
||||
common: commonJa,
|
||||
"game-misc": gameMiscJa,
|
||||
tournament: tournamentJa,
|
||||
user: userJa,
|
||||
q: qJa,
|
||||
art: artJa,
|
||||
builds: buildsJa,
|
||||
lfg: lfgJa,
|
||||
vods: vodsJa,
|
||||
calendar: calendarJa,
|
||||
org: orgJa,
|
||||
badges: badgesJa,
|
||||
contributions: contributionsJa,
|
||||
team: teamJa,
|
||||
analyzer: analyzerJa,
|
||||
},
|
||||
da: {
|
||||
gear: gearDa,
|
||||
faq: faqDa,
|
||||
weapons: weaponsDa,
|
||||
common: commonDa,
|
||||
"game-misc": gameMiscDa,
|
||||
tournament: tournamentDa,
|
||||
user: userDa,
|
||||
q: qDa,
|
||||
art: artDa,
|
||||
builds: buildsDa,
|
||||
lfg: lfgDa,
|
||||
vods: vodsDa,
|
||||
calendar: calendarDa,
|
||||
org: orgDa,
|
||||
badges: badgesDa,
|
||||
contributions: contributionsDa,
|
||||
team: teamDa,
|
||||
analyzer: analyzerDa,
|
||||
},
|
||||
"es-ES": {
|
||||
gear: gearEsEs,
|
||||
faq: faqEsEs,
|
||||
weapons: weaponsEsEs,
|
||||
common: commonEsEs,
|
||||
"game-misc": gameMiscEsEs,
|
||||
tournament: tournamentEsEs,
|
||||
user: userEsEs,
|
||||
q: qEsEs,
|
||||
art: artEsEs,
|
||||
builds: buildsEsEs,
|
||||
lfg: lfgEsEs,
|
||||
vods: vodsEsEs,
|
||||
calendar: calendarEsEs,
|
||||
org: orgEsEs,
|
||||
badges: badgesEsEs,
|
||||
contributions: contributionsEsEs,
|
||||
team: teamEsEs,
|
||||
analyzer: analyzerEsEs,
|
||||
},
|
||||
he: {
|
||||
gear: gearHe,
|
||||
faq: faqHe,
|
||||
weapons: weaponsHe,
|
||||
common: commonHe,
|
||||
"game-misc": gameMiscHe,
|
||||
tournament: tournamentHe,
|
||||
user: userHe,
|
||||
q: qHe,
|
||||
art: artHe,
|
||||
builds: buildsHe,
|
||||
lfg: lfgHe,
|
||||
vods: vodsHe,
|
||||
calendar: calendarHe,
|
||||
org: orgHe,
|
||||
badges: badgesHe,
|
||||
contributions: contributionsHe,
|
||||
team: teamHe,
|
||||
analyzer: analyzerHe,
|
||||
},
|
||||
"fr-EU": {
|
||||
gear: gearFrEu,
|
||||
faq: faqFrEu,
|
||||
weapons: weaponsFrEu,
|
||||
common: commonFrEu,
|
||||
"game-misc": gameMiscFrEu,
|
||||
tournament: tournamentFrEu,
|
||||
user: userFrEu,
|
||||
q: qFrEu,
|
||||
art: artFrEu,
|
||||
builds: buildsFrEu,
|
||||
lfg: lfgFrEu,
|
||||
vods: vodsFrEu,
|
||||
calendar: calendarFrEu,
|
||||
org: orgFrEu,
|
||||
badges: badgesFrEu,
|
||||
contributions: contributionsFrEu,
|
||||
team: teamFrEu,
|
||||
analyzer: analyzerFrEu,
|
||||
},
|
||||
pl: {
|
||||
gear: gearPl,
|
||||
faq: faqPl,
|
||||
weapons: weaponsPl,
|
||||
common: commonPl,
|
||||
"game-misc": gameMiscPl,
|
||||
tournament: tournamentPl,
|
||||
user: userPl,
|
||||
q: qPl,
|
||||
art: artPl,
|
||||
builds: buildsPl,
|
||||
lfg: lfgPl,
|
||||
vods: vodsPl,
|
||||
calendar: calendarPl,
|
||||
org: orgPl,
|
||||
badges: badgesPl,
|
||||
contributions: contributionsPl,
|
||||
team: teamPl,
|
||||
analyzer: analyzerPl,
|
||||
},
|
||||
};
|
||||
|
||||
export type Namespace = keyof typeof resources.en;
|
||||
@@ -1,165 +1,160 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { MainWeaponId } from "./types";
|
||||
import { weaponIdToBucketId } from "./weapon-ids";
|
||||
|
||||
const WeaponIdToBucketId = suite("weaponIdToBucketId()");
|
||||
describe("weaponIdToBucketId()", () => {
|
||||
test("Each weaponId is mapped to its correct bucket", () => {
|
||||
const mappings = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[10, 10],
|
||||
[11, 10],
|
||||
[20, 20],
|
||||
[21, 20],
|
||||
[30, 30],
|
||||
[31, 30],
|
||||
[40, 40],
|
||||
[41, 40],
|
||||
[45, 40],
|
||||
[46, 40],
|
||||
[47, 40],
|
||||
[50, 50],
|
||||
[51, 50],
|
||||
[60, 60],
|
||||
[61, 60],
|
||||
[70, 70],
|
||||
[71, 70],
|
||||
[80, 80],
|
||||
[81, 80],
|
||||
[90, 90],
|
||||
[91, 90],
|
||||
[100, 100],
|
||||
[101, 100],
|
||||
[200, 200],
|
||||
[201, 200],
|
||||
[205, 200],
|
||||
[210, 210],
|
||||
[211, 210],
|
||||
[220, 220],
|
||||
[221, 220],
|
||||
[230, 230],
|
||||
[231, 230],
|
||||
[240, 240],
|
||||
[241, 240],
|
||||
[250, 250],
|
||||
[251, 250],
|
||||
[260, 260],
|
||||
[261, 260],
|
||||
[300, 300],
|
||||
[301, 300],
|
||||
[310, 310],
|
||||
[311, 310],
|
||||
[400, 400],
|
||||
[401, 400],
|
||||
[1000, 1000],
|
||||
[1001, 1000],
|
||||
[1010, 1010],
|
||||
[1011, 1010],
|
||||
[1015, 1010],
|
||||
[1020, 1020],
|
||||
[1021, 1020],
|
||||
[1030, 1030],
|
||||
[1031, 1030],
|
||||
[1040, 1040],
|
||||
[1041, 1040],
|
||||
[1100, 1100],
|
||||
[1101, 1100],
|
||||
[1110, 1110],
|
||||
[1111, 1110],
|
||||
[1115, 1110],
|
||||
[1120, 1120],
|
||||
[1121, 1120],
|
||||
[2000, 2000],
|
||||
[2001, 2000],
|
||||
[2010, 2010],
|
||||
[2011, 2010],
|
||||
[2015, 2010],
|
||||
[2020, 2020],
|
||||
[2021, 2020],
|
||||
[2030, 2030],
|
||||
[2031, 2030],
|
||||
[2040, 2040],
|
||||
[2041, 2040],
|
||||
[2050, 2050],
|
||||
[2051, 2050],
|
||||
[2060, 2060],
|
||||
[2061, 2060],
|
||||
[2070, 2070],
|
||||
[2071, 2070],
|
||||
[3000, 3000],
|
||||
[3001, 3000],
|
||||
[3005, 3000],
|
||||
[3010, 3010],
|
||||
[3011, 3010],
|
||||
[3020, 3020],
|
||||
[3021, 3020],
|
||||
[3030, 3030],
|
||||
[3031, 3030],
|
||||
[3040, 3040],
|
||||
[3041, 3040],
|
||||
[3050, 3050],
|
||||
[3051, 3050],
|
||||
[4000, 4000],
|
||||
[4001, 4000],
|
||||
[4010, 4010],
|
||||
[4011, 4010],
|
||||
[4015, 4010],
|
||||
[4020, 4020],
|
||||
[4021, 4020],
|
||||
[4030, 4030],
|
||||
[4031, 4030],
|
||||
[4040, 4040],
|
||||
[4041, 4040],
|
||||
[4050, 4050],
|
||||
[4051, 4050],
|
||||
[5000, 5000],
|
||||
[5001, 5000],
|
||||
[5010, 5010],
|
||||
[5011, 5010],
|
||||
[5015, 5010],
|
||||
[5020, 5020],
|
||||
[5021, 5020],
|
||||
[5030, 5030],
|
||||
[5031, 5030],
|
||||
[5040, 5040],
|
||||
[5041, 5040],
|
||||
[5050, 5050],
|
||||
[5051, 5050],
|
||||
[6000, 6000],
|
||||
[6001, 6000],
|
||||
[6005, 6000],
|
||||
[6010, 6010],
|
||||
[6011, 6010],
|
||||
[6020, 6020],
|
||||
[6021, 6020],
|
||||
[6030, 6030],
|
||||
[6031, 6030],
|
||||
[7010, 7010],
|
||||
[7011, 7010],
|
||||
[7015, 7010],
|
||||
[7020, 7020],
|
||||
[7021, 7020],
|
||||
[7030, 7030],
|
||||
[7031, 7030],
|
||||
[8000, 8000],
|
||||
[8001, 8000],
|
||||
[8005, 8000],
|
||||
[8010, 8010],
|
||||
[8011, 8010],
|
||||
[8020, 8020],
|
||||
[8021, 8020],
|
||||
];
|
||||
for (const [id, expected] of mappings) {
|
||||
expect(weaponIdToBucketId(id as MainWeaponId)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
WeaponIdToBucketId("Each weaponId is mapped to its correct bucket", () => {
|
||||
const mappings = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[10, 10],
|
||||
[11, 10],
|
||||
[20, 20],
|
||||
[21, 20],
|
||||
[30, 30],
|
||||
[31, 30],
|
||||
[40, 40],
|
||||
[41, 40],
|
||||
[45, 40],
|
||||
[46, 40],
|
||||
[47, 40],
|
||||
[50, 50],
|
||||
[51, 50],
|
||||
[60, 60],
|
||||
[61, 60],
|
||||
[70, 70],
|
||||
[71, 70],
|
||||
[80, 80],
|
||||
[81, 80],
|
||||
[90, 90],
|
||||
[91, 90],
|
||||
[100, 100],
|
||||
[101, 100],
|
||||
[200, 200],
|
||||
[201, 200],
|
||||
[205, 200],
|
||||
[210, 210],
|
||||
[211, 210],
|
||||
[220, 220],
|
||||
[221, 220],
|
||||
[230, 230],
|
||||
[231, 230],
|
||||
[240, 240],
|
||||
[241, 240],
|
||||
[250, 250],
|
||||
[251, 250],
|
||||
[260, 260],
|
||||
[261, 260],
|
||||
[300, 300],
|
||||
[301, 300],
|
||||
[310, 310],
|
||||
[311, 310],
|
||||
[400, 400],
|
||||
[401, 400],
|
||||
[1000, 1000],
|
||||
[1001, 1000],
|
||||
[1010, 1010],
|
||||
[1011, 1010],
|
||||
[1015, 1010],
|
||||
[1020, 1020],
|
||||
[1021, 1020],
|
||||
[1030, 1030],
|
||||
[1031, 1030],
|
||||
[1040, 1040],
|
||||
[1041, 1040],
|
||||
[1100, 1100],
|
||||
[1101, 1100],
|
||||
[1110, 1110],
|
||||
[1111, 1110],
|
||||
[1115, 1110],
|
||||
[1120, 1120],
|
||||
[1121, 1120],
|
||||
[2000, 2000],
|
||||
[2001, 2000],
|
||||
[2010, 2010],
|
||||
[2011, 2010],
|
||||
[2015, 2010],
|
||||
[2020, 2020],
|
||||
[2021, 2020],
|
||||
[2030, 2030],
|
||||
[2031, 2030],
|
||||
[2040, 2040],
|
||||
[2041, 2040],
|
||||
[2050, 2050],
|
||||
[2051, 2050],
|
||||
[2060, 2060],
|
||||
[2061, 2060],
|
||||
[2070, 2070],
|
||||
[2071, 2070],
|
||||
[3000, 3000],
|
||||
[3001, 3000],
|
||||
[3005, 3000],
|
||||
[3010, 3010],
|
||||
[3011, 3010],
|
||||
[3020, 3020],
|
||||
[3021, 3020],
|
||||
[3030, 3030],
|
||||
[3031, 3030],
|
||||
[3040, 3040],
|
||||
[3041, 3040],
|
||||
[3050, 3050],
|
||||
[3051, 3050],
|
||||
[4000, 4000],
|
||||
[4001, 4000],
|
||||
[4010, 4010],
|
||||
[4011, 4010],
|
||||
[4015, 4010],
|
||||
[4020, 4020],
|
||||
[4021, 4020],
|
||||
[4030, 4030],
|
||||
[4031, 4030],
|
||||
[4040, 4040],
|
||||
[4041, 4040],
|
||||
[4050, 4050],
|
||||
[4051, 4050],
|
||||
[5000, 5000],
|
||||
[5001, 5000],
|
||||
[5010, 5010],
|
||||
[5011, 5010],
|
||||
[5015, 5010],
|
||||
[5020, 5020],
|
||||
[5021, 5020],
|
||||
[5030, 5030],
|
||||
[5031, 5030],
|
||||
[5040, 5040],
|
||||
[5041, 5040],
|
||||
[5050, 5050],
|
||||
[5051, 5050],
|
||||
[6000, 6000],
|
||||
[6001, 6000],
|
||||
[6005, 6000],
|
||||
[6010, 6010],
|
||||
[6011, 6010],
|
||||
[6020, 6020],
|
||||
[6021, 6020],
|
||||
[6030, 6030],
|
||||
[6031, 6030],
|
||||
[7010, 7010],
|
||||
[7011, 7010],
|
||||
[7015, 7010],
|
||||
[7020, 7020],
|
||||
[7021, 7020],
|
||||
[7030, 7030],
|
||||
[7031, 7030],
|
||||
[8000, 8000],
|
||||
[8001, 8000],
|
||||
[8005, 8000],
|
||||
[8010, 8010],
|
||||
[8011, 8010],
|
||||
[8020, 8020],
|
||||
[8021, 8020],
|
||||
];
|
||||
for (const [id, expected] of mappings) {
|
||||
assert.is(weaponIdToBucketId(id as MainWeaponId), expected);
|
||||
}
|
||||
});
|
||||
|
||||
WeaponIdToBucketId(
|
||||
"Buckets represent the same main weapon across variants",
|
||||
() => {
|
||||
test("Buckets represent the same main weapon across variants", () => {
|
||||
const weaponData: Record<MainWeaponId, any> = {
|
||||
"250": {
|
||||
season: 0,
|
||||
@@ -1462,10 +1457,8 @@ WeaponIdToBucketId(
|
||||
if (main[bucket] === undefined) {
|
||||
main[bucket] = weaponPrefix(kit);
|
||||
} else {
|
||||
assert.is(main[bucket], weaponPrefix(kit));
|
||||
expect(main[bucket]).toBe(weaponPrefix(kit));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
WeaponIdToBucketId.run();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import { createTournamentMapList } from ".";
|
||||
import type { RankedModeShort } from "../in-game-lists";
|
||||
@@ -7,11 +6,6 @@ import { rankedModesShort } from "../in-game-lists/modes";
|
||||
import { DEFAULT_MAP_POOL } from "./constants";
|
||||
import type { TournamentMaplistInput } from "./types";
|
||||
|
||||
const TournamentMapListGenerator = suite("Tournament map list generator");
|
||||
const TournamentMapListGeneratorOneMode = suite(
|
||||
"Tournament map list generator (one mode)",
|
||||
);
|
||||
|
||||
const team1Picks = new MapPool([
|
||||
{ mode: "SZ", stageId: 4 },
|
||||
{ mode: "SZ", stageId: 5 },
|
||||
@@ -49,6 +43,23 @@ const tiebreakerPicks = new MapPool([
|
||||
{ mode: "CB", stageId: 4 },
|
||||
]);
|
||||
|
||||
const duplicationPicks = new MapPool([
|
||||
{ mode: "SZ", stageId: 4 },
|
||||
{ mode: "SZ", stageId: 5 },
|
||||
{ mode: "TC", stageId: 4 },
|
||||
{ mode: "TC", stageId: 5 },
|
||||
{ mode: "RM", stageId: 6 },
|
||||
{ mode: "RM", stageId: 7 },
|
||||
{ mode: "CB", stageId: 6 },
|
||||
{ mode: "CB", stageId: 7 },
|
||||
]);
|
||||
const duplicationTiebreaker = new MapPool([
|
||||
{ mode: "SZ", stageId: 7 },
|
||||
{ mode: "TC", stageId: 6 },
|
||||
{ mode: "RM", stageId: 5 },
|
||||
{ mode: "CB", stageId: 4 },
|
||||
]);
|
||||
|
||||
const generateMaps = ({
|
||||
count = 5,
|
||||
seed = "test",
|
||||
@@ -76,84 +87,80 @@ const generateMaps = ({
|
||||
});
|
||||
};
|
||||
|
||||
TournamentMapListGenerator("Modes are spread evenly", () => {
|
||||
const mapList = generateMaps();
|
||||
const modes = new Set(rankedModesShort);
|
||||
describe("Tournament map list generator", () => {
|
||||
test("Modes are spread evenly", () => {
|
||||
const mapList = generateMaps();
|
||||
const modes = new Set(rankedModesShort);
|
||||
|
||||
assert.equal(mapList.length, 5);
|
||||
expect(mapList.length).toBe(5);
|
||||
|
||||
for (const [i, { mode }] of mapList.entries()) {
|
||||
const rankedMode = mode as RankedModeShort;
|
||||
if (!modes.has(rankedMode)) {
|
||||
assert.equal(i, 4, "Repeated mode early");
|
||||
assert.equal(mode, mapList[0].mode, "1st and 5th mode are not the same");
|
||||
for (const [i, { mode }] of mapList.entries()) {
|
||||
const rankedMode = mode as RankedModeShort;
|
||||
if (!modes.has(rankedMode)) {
|
||||
expect(i).toBe(4);
|
||||
expect(mode).toBe(mapList[0].mode);
|
||||
}
|
||||
|
||||
modes.delete(rankedMode);
|
||||
}
|
||||
});
|
||||
|
||||
test("Follow mode order option", () => {
|
||||
const mapList = generateMaps({ followModeOrder: true });
|
||||
|
||||
expect(mapList[0].mode).toBe("SZ");
|
||||
expect(mapList[1].mode).toBe("TC");
|
||||
expect(mapList[2].mode).toBe("RM");
|
||||
expect(mapList[3].mode).toBe("CB");
|
||||
expect(mapList[4].mode).toBe("SZ");
|
||||
});
|
||||
|
||||
test("Equal picks", () => {
|
||||
let our = 0;
|
||||
let their = 0;
|
||||
let tiebreaker = 0;
|
||||
|
||||
const mapList = generateMaps();
|
||||
|
||||
for (const { stageId, mode } of mapList) {
|
||||
if (team1Picks.has({ stageId, mode })) {
|
||||
our++;
|
||||
}
|
||||
|
||||
if (team2Picks.has({ stageId, mode })) {
|
||||
their++;
|
||||
}
|
||||
|
||||
if (tiebreakerPicks.has({ stageId, mode })) {
|
||||
tiebreaker++;
|
||||
}
|
||||
}
|
||||
|
||||
modes.delete(rankedMode);
|
||||
}
|
||||
});
|
||||
expect(our).toBe(their);
|
||||
expect(tiebreaker).toBe(1);
|
||||
});
|
||||
|
||||
TournamentMapListGenerator("Follow mode order option", () => {
|
||||
const mapList = generateMaps({ followModeOrder: true });
|
||||
test("No stage repeats in optimal case", () => {
|
||||
const mapList = generateMaps();
|
||||
|
||||
assert.equal(mapList[0].mode, "SZ");
|
||||
assert.equal(mapList[1].mode, "TC");
|
||||
assert.equal(mapList[2].mode, "RM");
|
||||
assert.equal(mapList[3].mode, "CB");
|
||||
assert.equal(mapList[4].mode, "SZ");
|
||||
});
|
||||
const stages = new Set(mapList.map(({ stageId }) => stageId));
|
||||
|
||||
TournamentMapListGenerator("Equal picks", () => {
|
||||
let our = 0;
|
||||
let their = 0;
|
||||
let tiebreaker = 0;
|
||||
expect(stages.size).toBe(5);
|
||||
});
|
||||
|
||||
const mapList = generateMaps();
|
||||
|
||||
for (const { stageId, mode } of mapList) {
|
||||
if (team1Picks.has({ stageId, mode })) {
|
||||
our++;
|
||||
}
|
||||
|
||||
if (team2Picks.has({ stageId, mode })) {
|
||||
their++;
|
||||
}
|
||||
|
||||
if (tiebreakerPicks.has({ stageId, mode })) {
|
||||
tiebreaker++;
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(our, their);
|
||||
assert.equal(tiebreaker, 1);
|
||||
});
|
||||
|
||||
TournamentMapListGenerator("No stage repeats in optimal case", () => {
|
||||
const mapList = generateMaps();
|
||||
|
||||
const stages = new Set(mapList.map(({ stageId }) => stageId));
|
||||
|
||||
assert.equal(stages.size, 5);
|
||||
});
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"Always generates same maplist given same input",
|
||||
() => {
|
||||
test("Always generates same maplist given same input", () => {
|
||||
const mapList1 = generateMaps();
|
||||
const mapList2 = generateMaps();
|
||||
|
||||
assert.equal(mapList1.length, 5);
|
||||
expect(mapList1.length).toBe(5);
|
||||
|
||||
for (let i = 0; i < mapList1.length; i++) {
|
||||
assert.equal(mapList1[i].stageId, mapList2[i].stageId);
|
||||
assert.equal(mapList1[i].mode, mapList2[i].mode);
|
||||
expect(mapList1[i].stageId).toBe(mapList2[i].stageId);
|
||||
expect(mapList1[i].mode).toBe(mapList2[i].mode);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"Order of team doesn't matter regarding what maplist gets created",
|
||||
() => {
|
||||
test("Order of team doesn't matter regarding what maplist gets created", () => {
|
||||
const mapList1 = generateMaps();
|
||||
const mapList2 = generateMaps({
|
||||
teams: [
|
||||
@@ -168,18 +175,15 @@ TournamentMapListGenerator(
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(mapList1.length, 5);
|
||||
expect(mapList1.length).toBe(5);
|
||||
|
||||
for (let i = 0; i < mapList1.length; i++) {
|
||||
assert.equal(mapList1[i].stageId, mapList2[i].stageId);
|
||||
assert.equal(mapList1[i].mode, mapList2[i].mode);
|
||||
expect(mapList1[i].stageId).toBe(mapList2[i].stageId);
|
||||
expect(mapList1[i].mode).toBe(mapList2[i].mode);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"Order of maps in the list doesn't matter regarding what maplist gets created",
|
||||
() => {
|
||||
test("Order of maps in the list doesn't matter regarding what maplist gets created", () => {
|
||||
const mapList1 = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -205,35 +209,15 @@ TournamentMapListGenerator(
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(mapList1.length, 5);
|
||||
expect(mapList1.length).toBe(5);
|
||||
|
||||
for (let i = 0; i < mapList1.length; i++) {
|
||||
assert.equal(mapList1[i].stageId, mapList2[i].stageId);
|
||||
assert.equal(mapList1[i].mode, mapList2[i].mode);
|
||||
expect(mapList1[i].stageId).toBe(mapList2[i].stageId);
|
||||
expect(mapList1[i].mode).toBe(mapList2[i].mode);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const duplicationPicks = new MapPool([
|
||||
{ mode: "SZ", stageId: 4 },
|
||||
{ mode: "SZ", stageId: 5 },
|
||||
{ mode: "TC", stageId: 4 },
|
||||
{ mode: "TC", stageId: 5 },
|
||||
{ mode: "RM", stageId: 6 },
|
||||
{ mode: "RM", stageId: 7 },
|
||||
{ mode: "CB", stageId: 6 },
|
||||
{ mode: "CB", stageId: 7 },
|
||||
]);
|
||||
const duplicationTiebreaker = new MapPool([
|
||||
{ mode: "SZ", stageId: 7 },
|
||||
{ mode: "TC", stageId: 6 },
|
||||
{ mode: "RM", stageId: 5 },
|
||||
{ mode: "CB", stageId: 4 },
|
||||
]);
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"Uses other teams maps if one didn't submit maplist",
|
||||
() => {
|
||||
test("Uses other teams maps if one didn't submit maplist", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -247,21 +231,20 @@ TournamentMapListGenerator(
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(mapList.length, 5);
|
||||
expect(mapList.length).toBe(5);
|
||||
|
||||
for (let i = 0; i < mapList.length - 1; i++) {
|
||||
// map belongs to team 2 map list
|
||||
const map = mapList[i];
|
||||
assert.ok(map);
|
||||
expect(map).toBeTruthy();
|
||||
|
||||
team2Picks.has({ mode: map.mode, stageId: map.stageId });
|
||||
expect(team2Picks.has({ mode: map.mode, stageId: map.stageId })).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"Creates map list even if neither team submitted maps",
|
||||
() => {
|
||||
test("Creates map list even if neither team submitted maps", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -275,84 +258,49 @@ TournamentMapListGenerator(
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(mapList.length, 5);
|
||||
},
|
||||
);
|
||||
|
||||
TournamentMapListGenerator("Handles worst case with duplication", () => {
|
||||
const maplist = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: duplicationPicks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: duplicationPicks,
|
||||
},
|
||||
],
|
||||
count: 7,
|
||||
tiebreakerMaps: duplicationTiebreaker,
|
||||
expect(mapList.length).toBe(5);
|
||||
});
|
||||
|
||||
assert.equal(maplist.length, 7);
|
||||
test("Handles worst case with duplication", () => {
|
||||
const maplist = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: duplicationPicks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: duplicationPicks,
|
||||
},
|
||||
],
|
||||
count: 7,
|
||||
tiebreakerMaps: duplicationTiebreaker,
|
||||
});
|
||||
|
||||
// all stages appear
|
||||
const stages = new Set(maplist.map(({ stageId }) => stageId));
|
||||
assert.equal(stages.size, 4);
|
||||
expect(maplist.length).toBe(7);
|
||||
|
||||
// no consecutive stage replays
|
||||
for (let i = 0; i < maplist.length - 1; i++) {
|
||||
assert.not.equal(maplist[i].stageId, maplist[i + 1].stageId);
|
||||
}
|
||||
});
|
||||
// all stages appear
|
||||
const stages = new Set(maplist.map(({ stageId }) => stageId));
|
||||
expect(stages.size).toBe(4);
|
||||
|
||||
const team2PicksWithSomeDuplication = new MapPool([
|
||||
{ mode: "SZ", stageId: 4 },
|
||||
{ mode: "SZ", stageId: 11 },
|
||||
{ mode: "TC", stageId: 5 },
|
||||
{ mode: "TC", stageId: 6 },
|
||||
{ mode: "RM", stageId: 7 },
|
||||
{ mode: "RM", stageId: 2 },
|
||||
{ mode: "CB", stageId: 9 },
|
||||
{ mode: "CB", stageId: 10 },
|
||||
]);
|
||||
|
||||
TournamentMapListGenerator("Keeps things fair when overlap", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: team1Picks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: team2PicksWithSomeDuplication,
|
||||
},
|
||||
],
|
||||
count: 7,
|
||||
// no consecutive stage replays
|
||||
for (let i = 0; i < maplist.length - 1; i++) {
|
||||
expect(maplist[i].stageId).not.toBe(maplist[i + 1].stageId);
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(mapList.length, 7);
|
||||
const team2PicksWithSomeDuplication = new MapPool([
|
||||
{ mode: "SZ", stageId: 4 },
|
||||
{ mode: "SZ", stageId: 11 },
|
||||
{ mode: "TC", stageId: 5 },
|
||||
{ mode: "TC", stageId: 6 },
|
||||
{ mode: "RM", stageId: 7 },
|
||||
{ mode: "RM", stageId: 2 },
|
||||
{ mode: "CB", stageId: 9 },
|
||||
{ mode: "CB", stageId: 10 },
|
||||
]);
|
||||
|
||||
let team1PicksAppeared = 0;
|
||||
let team2PicksAppeared = 0;
|
||||
|
||||
for (const { stageId, mode } of mapList) {
|
||||
if (team1Picks.has({ stageId, mode })) {
|
||||
team1PicksAppeared++;
|
||||
}
|
||||
|
||||
if (team2PicksWithSomeDuplication.has({ stageId, mode })) {
|
||||
team2PicksAppeared++;
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(team1PicksAppeared, team2PicksAppeared);
|
||||
});
|
||||
|
||||
TournamentMapListGenerator("No map picked by same team twice in row", () => {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
test("Keeps things fair when overlap", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -361,22 +309,54 @@ TournamentMapListGenerator("No map picked by same team twice in row", () => {
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: team2Picks,
|
||||
maps: team2PicksWithSomeDuplication,
|
||||
},
|
||||
],
|
||||
seed: String(i),
|
||||
count: 7,
|
||||
});
|
||||
|
||||
for (let j = 0; j < mapList.length - 1; j++) {
|
||||
if (typeof mapList[j].source !== "number") continue;
|
||||
assert.not.equal(mapList[j].source, mapList[j + 1].source);
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(mapList.length).toBe(7);
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"Calculates all mode maps without tiebreaker",
|
||||
() => {
|
||||
let team1PicksAppeared = 0;
|
||||
let team2PicksAppeared = 0;
|
||||
|
||||
for (const { stageId, mode } of mapList) {
|
||||
if (team1Picks.has({ stageId, mode })) {
|
||||
team1PicksAppeared++;
|
||||
}
|
||||
|
||||
if (team2PicksWithSomeDuplication.has({ stageId, mode })) {
|
||||
team2PicksAppeared++;
|
||||
}
|
||||
}
|
||||
|
||||
expect(team1PicksAppeared).toBe(team2PicksAppeared);
|
||||
});
|
||||
|
||||
test("No map picked by same team twice in row", () => {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: team1Picks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: team2Picks,
|
||||
},
|
||||
],
|
||||
seed: String(i),
|
||||
});
|
||||
|
||||
for (let j = 0; j < mapList.length - 1; j++) {
|
||||
if (typeof mapList[j].source !== "number") continue;
|
||||
expect(mapList[j].source).not.toBe(mapList[j + 1].source);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Calculates all mode maps without tiebreaker", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -393,14 +373,11 @@ TournamentMapListGenerator(
|
||||
});
|
||||
|
||||
// the one map both of them picked
|
||||
assert.equal(mapList[6].stageId, 7);
|
||||
assert.equal(mapList[6].mode, "RM");
|
||||
},
|
||||
);
|
||||
expect(mapList[6].stageId).toBe(7);
|
||||
expect(mapList[6].mode).toBe("RM");
|
||||
});
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"Calculates all mode maps without tiebreaker (no overlap)",
|
||||
() => {
|
||||
test("Calculates all mode maps without tiebreaker (no overlap)", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -417,77 +394,71 @@ TournamentMapListGenerator(
|
||||
});
|
||||
|
||||
// default map pool contains the tiebreaker
|
||||
assert.ok(
|
||||
expect(
|
||||
DEFAULT_MAP_POOL.stageModePairs.some(
|
||||
(pair) =>
|
||||
pair.stageId === mapList[6].stageId && pair.mode === mapList[6].mode,
|
||||
),
|
||||
);
|
||||
).toBe(true);
|
||||
|
||||
// neither teams map pool contains the tiebreaker
|
||||
assert.not.ok(
|
||||
expect(
|
||||
team1Picks.stageModePairs.some(
|
||||
(pair) =>
|
||||
pair.stageId === mapList[6].stageId && pair.mode === mapList[6].mode,
|
||||
),
|
||||
);
|
||||
assert.not.ok(
|
||||
).toBe(false);
|
||||
expect(
|
||||
team2PicksNoOverlap.stageModePairs.some(
|
||||
(pair) =>
|
||||
pair.stageId === mapList[6].stageId && pair.mode === mapList[6].mode,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
const threeModesArgs: TournamentMaplistInput = {
|
||||
count: 7,
|
||||
seed: "1002",
|
||||
modesIncluded: ["TC", "TW", "RM"],
|
||||
tiebreakerMaps: new MapPool({
|
||||
TW: [],
|
||||
SZ: [],
|
||||
TC: [],
|
||||
RM: [],
|
||||
CB: [],
|
||||
}),
|
||||
teams: [
|
||||
{
|
||||
id: 1002,
|
||||
maps: new MapPool({
|
||||
TW: [9, 7, 6, 5, 3, 2, 0],
|
||||
SZ: [],
|
||||
TC: [9, 8, 7, 4, 1, 6, 2],
|
||||
RM: [9, 7, 6, 5, 3, 1, 0],
|
||||
CB: [],
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
maps: new MapPool({
|
||||
TW: [8, 7, 5, 2, 9, 4, 3],
|
||||
SZ: [],
|
||||
TC: [7, 6, 5, 3, 2, 0, 9],
|
||||
RM: [9, 8, 6, 5, 3, 2, 7],
|
||||
CB: [],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
const threeModesArgs: TournamentMaplistInput = {
|
||||
count: 7,
|
||||
seed: "1002",
|
||||
modesIncluded: ["TC", "TW", "RM"],
|
||||
tiebreakerMaps: new MapPool({
|
||||
TW: [],
|
||||
SZ: [],
|
||||
TC: [],
|
||||
RM: [],
|
||||
CB: [],
|
||||
}),
|
||||
teams: [
|
||||
{
|
||||
id: 1002,
|
||||
maps: new MapPool({
|
||||
TW: [9, 7, 6, 5, 3, 2, 0],
|
||||
SZ: [],
|
||||
TC: [9, 8, 7, 4, 1, 6, 2],
|
||||
RM: [9, 7, 6, 5, 3, 1, 0],
|
||||
CB: [],
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
maps: new MapPool({
|
||||
TW: [8, 7, 5, 2, 9, 4, 3],
|
||||
SZ: [],
|
||||
TC: [7, 6, 5, 3, 2, 0, 9],
|
||||
RM: [9, 8, 6, 5, 3, 2, 7],
|
||||
CB: [],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
TournamentMapListGenerator(
|
||||
"generates list of modes included length > 1 && < 4",
|
||||
() => {
|
||||
test("generates list of modes included length > 1 && < 4", () => {
|
||||
const maps = generateMaps(threeModesArgs);
|
||||
|
||||
assert.equal(maps.length, 7);
|
||||
},
|
||||
);
|
||||
expect(maps.length).toBe(7);
|
||||
});
|
||||
|
||||
// paddling pool 264
|
||||
TournamentMapListGenerator(
|
||||
"handles 100% overlap in one mode and none in others",
|
||||
() => {
|
||||
// paddling pool 264
|
||||
test("handles 100% overlap in one mode and none in others", () => {
|
||||
// should not throw
|
||||
generateMaps({
|
||||
count: 5,
|
||||
@@ -589,8 +560,8 @@ TournamentMapListGenerator(
|
||||
},
|
||||
]),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const team1SZPicks = new MapPool([
|
||||
{ mode: "SZ", stageId: 4 },
|
||||
@@ -617,9 +588,8 @@ const team2SZPicksNoOverlap = new MapPool([
|
||||
{ mode: "SZ", stageId: 11 },
|
||||
]);
|
||||
|
||||
TournamentMapListGeneratorOneMode(
|
||||
"Creates map list for one mode inferring from the team picks",
|
||||
() => {
|
||||
describe("TournamentMapListGeneratorOneMode", () => {
|
||||
test("Creates map list for one mode inferring from the team picks", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -635,14 +605,11 @@ TournamentMapListGeneratorOneMode(
|
||||
tiebreakerMaps: new MapPool([]),
|
||||
});
|
||||
for (let i = 0; i < mapList.length - 1; i++) {
|
||||
assert.equal(mapList[i].mode, "SZ");
|
||||
expect(mapList[i].mode).toBe("SZ");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
TournamentMapListGeneratorOneMode(
|
||||
"Creates one mode map list from empty map lists",
|
||||
() => {
|
||||
test("Creates one mode map list from empty map lists", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -658,14 +625,11 @@ TournamentMapListGeneratorOneMode(
|
||||
tiebreakerMaps: new MapPool([]),
|
||||
});
|
||||
for (let i = 0; i < mapList.length - 1; i++) {
|
||||
assert.equal(mapList[i].mode, "SZ");
|
||||
expect(mapList[i].mode).toBe("SZ");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
TournamentMapListGeneratorOneMode(
|
||||
"Creates all different maps from empty map lists",
|
||||
() => {
|
||||
test("Creates all different maps from empty map lists", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -682,13 +646,10 @@ TournamentMapListGeneratorOneMode(
|
||||
});
|
||||
|
||||
const stages = new Set(mapList.map(({ stageId }) => stageId));
|
||||
assert.equal(stages.size, 5);
|
||||
},
|
||||
);
|
||||
expect(stages.size).toBe(5);
|
||||
});
|
||||
|
||||
TournamentMapListGeneratorOneMode(
|
||||
"Tiebreaker is always from the maps of the teams when possible",
|
||||
() => {
|
||||
test("Tiebreaker is always from the maps of the teams when possible", () => {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
@@ -708,15 +669,12 @@ TournamentMapListGeneratorOneMode(
|
||||
|
||||
const last = mapList[mapList.length - 1];
|
||||
|
||||
assert.equal(last?.mode, "SZ");
|
||||
assert.equal(last?.stageId, 9);
|
||||
expect(last?.mode).toBe("SZ");
|
||||
expect(last?.stageId).toBe(9);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
TournamentMapListGeneratorOneMode(
|
||||
"Tiebreaker is from neither team's pool if no overlap",
|
||||
() => {
|
||||
test("Tiebreaker is from neither team's pool if no overlap", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -734,70 +692,67 @@ TournamentMapListGeneratorOneMode(
|
||||
|
||||
const last = mapList[mapList.length - 1];
|
||||
|
||||
assert.not.ok(
|
||||
expect(
|
||||
team1SZPicks.stageModePairs.some(
|
||||
({ stageId }) => stageId === last?.stageId,
|
||||
),
|
||||
);
|
||||
assert.not.ok(
|
||||
).toBe(false);
|
||||
expect(
|
||||
team2SZPicksNoOverlap.stageModePairs.some(
|
||||
({ stageId }) => stageId === last?.stageId,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TournamentMapListGeneratorOneMode("Handles worst case duplication", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: team1SZPicks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: team1SZPicks,
|
||||
},
|
||||
],
|
||||
modesIncluded: ["SZ"],
|
||||
tiebreakerMaps: new MapPool([]),
|
||||
count: 7,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
for (const [i, stage] of mapList.entries()) {
|
||||
if (i === 6) {
|
||||
assert.equal(stage?.source, "TIEBREAKER");
|
||||
} else {
|
||||
assert.equal(stage?.source, "BOTH");
|
||||
test("Handles worst case duplication", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: team1SZPicks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: team1SZPicks,
|
||||
},
|
||||
],
|
||||
modesIncluded: ["SZ"],
|
||||
tiebreakerMaps: new MapPool([]),
|
||||
count: 7,
|
||||
});
|
||||
|
||||
for (const [i, stage] of mapList.entries()) {
|
||||
if (i === 6) {
|
||||
expect(stage?.source).toBe("TIEBREAKER");
|
||||
} else {
|
||||
expect(stage?.source).toBe("BOTH");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TournamentMapListGeneratorOneMode("Handles one team submitted no maps", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: team1SZPicks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: new MapPool([]),
|
||||
},
|
||||
],
|
||||
modesIncluded: ["SZ"],
|
||||
tiebreakerMaps: new MapPool([]),
|
||||
});
|
||||
|
||||
for (const stage of mapList) {
|
||||
assert.equal(stage.source, 1);
|
||||
}
|
||||
});
|
||||
test("Handles one team submitted no maps", () => {
|
||||
const mapList = generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: team1SZPicks,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: new MapPool([]),
|
||||
},
|
||||
],
|
||||
modesIncluded: ["SZ"],
|
||||
tiebreakerMaps: new MapPool([]),
|
||||
});
|
||||
|
||||
TournamentMapListGeneratorOneMode(
|
||||
'Throws if including modes not specified in "modesIncluded"',
|
||||
() => {
|
||||
assert.throws(() =>
|
||||
for (const stage of mapList) {
|
||||
expect(stage.source).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('Throws if including modes not specified in "modesIncluded"', () => {
|
||||
expect(() =>
|
||||
generateMaps({
|
||||
teams: [
|
||||
{
|
||||
@@ -811,36 +766,27 @@ TournamentMapListGeneratorOneMode(
|
||||
],
|
||||
modesIncluded: ["SZ"],
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
TournamentMapListGeneratorOneMode(
|
||||
"Throws if duplicate maps in the pool",
|
||||
() => {
|
||||
assert.throws(
|
||||
() =>
|
||||
generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: new MapPool([
|
||||
{ mode: "SZ", stageId: 1 },
|
||||
{ mode: "SZ", stageId: 1 },
|
||||
]),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: new MapPool([]),
|
||||
},
|
||||
],
|
||||
modesIncluded: ["SZ"],
|
||||
}),
|
||||
(err: Error) => err.message.includes("Duplicate map"),
|
||||
"Expected error to be thrown about duplicate maps",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TournamentMapListGenerator.run();
|
||||
TournamentMapListGeneratorOneMode.run();
|
||||
test("Throws if duplicate maps in the pool", () => {
|
||||
expect(() =>
|
||||
generateMaps({
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
maps: new MapPool([
|
||||
{ mode: "SZ", stageId: 1 },
|
||||
{ mode: "SZ", stageId: 1 },
|
||||
]),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
maps: new MapPool([]),
|
||||
},
|
||||
],
|
||||
modesIncluded: ["SZ"],
|
||||
}),
|
||||
).toThrowError("Duplicate map");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import generalI18next from "i18next";
|
||||
import NProgress from "nprogress";
|
||||
import * as React from "react";
|
||||
import { type CustomTypeOptions, useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useChangeLanguage } from "remix-i18next/react";
|
||||
import type { SendouRouteHandle } from "~/utils/remix";
|
||||
import { Catcher } from "./components/Catcher";
|
||||
@@ -37,6 +37,7 @@ import { getThemeSession } from "./features/theme/core/session.server";
|
||||
import { useIsMounted } from "./hooks/useIsMounted";
|
||||
import { DEFAULT_LANGUAGE } from "./modules/i18n/config";
|
||||
import i18next, { i18nCookie } from "./modules/i18n/i18next.server";
|
||||
import type { Namespace } from "./modules/i18n/resources.server";
|
||||
import { COMMON_PREVIEW_IMAGE, SUSPENDED_PAGE } from "./utils/urls";
|
||||
|
||||
import "nprogress/nprogress.css";
|
||||
@@ -181,10 +182,7 @@ function useLoadingIndicator() {
|
||||
|
||||
// TODO: this should be an array if we can figure out how to make Typescript
|
||||
// enforce that it has every member of keyof CustomTypeOptions["resources"] without duplicating the type manually
|
||||
export const namespaceJsonsToPreloadObj: Record<
|
||||
keyof CustomTypeOptions["resources"],
|
||||
boolean
|
||||
> = {
|
||||
export const namespaceJsonsToPreloadObj: Record<Namespace, boolean> = {
|
||||
common: true,
|
||||
analyzer: true,
|
||||
badges: true,
|
||||
|
||||
@@ -92,29 +92,28 @@ async function authHeader(user?: "admin" | "regular"): Promise<HeadersInit> {
|
||||
return [["Cookie", await authSessionStorage.commitSession(session)]];
|
||||
}
|
||||
|
||||
export const database = {
|
||||
reset: () => {
|
||||
const tables = sql
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'migrations';",
|
||||
)
|
||||
.all() as { name: string }[];
|
||||
export const dbReset = () => {
|
||||
const tables = sql
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'migrations';",
|
||||
)
|
||||
.all() as { name: string }[];
|
||||
|
||||
sql.prepare("PRAGMA foreign_keys = OFF").run();
|
||||
for (const table of tables) {
|
||||
sql.prepare(`DELETE FROM "${table.name}"`).run();
|
||||
}
|
||||
sql.prepare("PRAGMA foreign_keys = ON").run();
|
||||
},
|
||||
insertUsers: (count: number) =>
|
||||
db
|
||||
.insertInto("User")
|
||||
.values(
|
||||
Array.from({ length: count }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
discordName: `user${i + 1}`,
|
||||
discordId: String(i),
|
||||
})),
|
||||
)
|
||||
.execute(),
|
||||
sql.prepare("PRAGMA foreign_keys = OFF").run();
|
||||
for (const table of tables) {
|
||||
sql.prepare(`DELETE FROM "${table.name}"`).run();
|
||||
}
|
||||
sql.prepare("PRAGMA foreign_keys = ON").run();
|
||||
};
|
||||
|
||||
export const dbInsertUsers = (count: number) =>
|
||||
db
|
||||
.insertInto("User")
|
||||
.values(
|
||||
Array.from({ length: count }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
discordName: `user${i + 1}`,
|
||||
discordId: String(i),
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// see vite.config.ts for explanation
|
||||
export default {};
|
||||
@@ -1,29 +0,0 @@
|
||||
const isEnabled =
|
||||
process.env.NEW_RELIC_APP_NAME && process.env.NEW_RELIC_LICENSE_KEY;
|
||||
|
||||
import newrelic from "newrelic";
|
||||
|
||||
export const noticeError = (
|
||||
error: Error,
|
||||
attributes?: {
|
||||
"enduser.id"?: number;
|
||||
formData?: string;
|
||||
searchParams?: string;
|
||||
params?: string;
|
||||
},
|
||||
) =>
|
||||
isEnabled &&
|
||||
newrelic.noticeError(error, {
|
||||
...attributes,
|
||||
"tags.commit": process.env.RENDER_GIT_COMMIT!,
|
||||
});
|
||||
|
||||
export const setTransactionName = (name: string) =>
|
||||
isEnabled && newrelic.setTransactionName(name);
|
||||
|
||||
export const ignoreTransaction = () => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
const transactionHandle = newrelic.getTransaction();
|
||||
transactionHandle.ignore();
|
||||
};
|
||||
@@ -1,34 +1,24 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { cutToNDecimalPlaces } from "./number";
|
||||
|
||||
const CutToNDecimalPlaces = suite("cutToNDecimalPlaces()");
|
||||
describe("cutToNDecimalPlaces()", () => {
|
||||
test("cutOff truncates decimal places correctly", () => {
|
||||
const result = cutToNDecimalPlaces(3.9999, 2);
|
||||
expect(result).toBe(3.99);
|
||||
});
|
||||
|
||||
CutToNDecimalPlaces("cutOff truncates decimal places correctly", () => {
|
||||
const result = cutToNDecimalPlaces(3.9999, 2);
|
||||
assert.is(result, 3.99);
|
||||
});
|
||||
test("cutOff can change amount of decimals returned", () => {
|
||||
const result = cutToNDecimalPlaces(3.12, 1);
|
||||
expect(result).toBe(3.1);
|
||||
});
|
||||
|
||||
CutToNDecimalPlaces("cutOff can change amount of decimals returned", () => {
|
||||
const result = cutToNDecimalPlaces(3.12, 1);
|
||||
assert.is(result, 3.1);
|
||||
});
|
||||
|
||||
CutToNDecimalPlaces(
|
||||
"cutOff preserves decimal values with the desired number of decimal places correctly",
|
||||
() => {
|
||||
test("cutOff preserves decimal values with the desired number of decimal places correctly", () => {
|
||||
const result = cutToNDecimalPlaces(100, 2);
|
||||
assert.is(result, 100);
|
||||
},
|
||||
);
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
|
||||
CutToNDecimalPlaces(
|
||||
"cutOff cuts off decimal places and removes trailing zeros correctly",
|
||||
() => {
|
||||
test("cutOff cuts off decimal places and removes trailing zeros correctly", () => {
|
||||
const result = cutToNDecimalPlaces(3.0001, 2);
|
||||
assert.is(result, 3);
|
||||
},
|
||||
);
|
||||
|
||||
CutToNDecimalPlaces.run();
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { z } from "zod";
|
||||
import type navItems from "~/components/layout/nav-items.json";
|
||||
import { s3UploadHandler } from "~/features/img-upload";
|
||||
import invariant from "./invariant";
|
||||
import { noticeError } from "./newrelic.server";
|
||||
|
||||
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
|
||||
if (!value) throw new Response(null, { status: 404 });
|
||||
@@ -54,7 +53,6 @@ export function parseSearchParams<T extends z.ZodTypeAny>({
|
||||
return schema.parse(searchParams);
|
||||
} catch (e) {
|
||||
if (e instanceof z.ZodError) {
|
||||
noticeError(e, { searchParams: JSON.stringify(searchParams) });
|
||||
console.error(e);
|
||||
throw new Response(JSON.stringify(e), { status: 400 });
|
||||
}
|
||||
@@ -96,7 +94,6 @@ export async function parseRequestPayload<T extends z.ZodTypeAny>({
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
if (e instanceof z.ZodError) {
|
||||
noticeError(e, { formData: JSON.stringify(formDataObj) });
|
||||
console.error(e);
|
||||
throw new Response(JSON.stringify(e), { status: 400 });
|
||||
}
|
||||
@@ -124,7 +121,6 @@ export async function parseFormData<T extends z.ZodTypeAny>({
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
if (e instanceof z.ZodError) {
|
||||
noticeError(e, { formData: JSON.stringify(formDataObj) });
|
||||
console.error(e);
|
||||
throw new Response(JSON.stringify(e), { status: 400 });
|
||||
}
|
||||
@@ -201,7 +197,6 @@ export function validate(
|
||||
): asserts condition {
|
||||
if (condition) return;
|
||||
|
||||
noticeError(new Error(`Validation error: ${message}`));
|
||||
throw new Response(
|
||||
message ? JSON.stringify({ validationError: message }) : undefined,
|
||||
{
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { pathnameFromPotentialURL } from "./strings";
|
||||
|
||||
const PathnameFromPotentialURL = suite("pathnameFromPotentialURL()");
|
||||
describe("pathnameFromPotentialURL()", () => {
|
||||
test("Resolves path name from valid URL", () => {
|
||||
expect(pathnameFromPotentialURL("https://twitter.com/sendouc")).toBe(
|
||||
"sendouc",
|
||||
);
|
||||
});
|
||||
|
||||
PathnameFromPotentialURL("Resolves path name from valid URL", () => {
|
||||
assert.is(pathnameFromPotentialURL("https://twitter.com/sendouc"), "sendouc");
|
||||
test("Returns string as is if not URL", () => {
|
||||
expect(pathnameFromPotentialURL("sendouc")).toBe("sendouc");
|
||||
});
|
||||
});
|
||||
|
||||
PathnameFromPotentialURL("Returns string as is if not URL", () => {
|
||||
assert.is(pathnameFromPotentialURL("sendouc"), "sendouc");
|
||||
});
|
||||
|
||||
PathnameFromPotentialURL.run();
|
||||
|
||||
@@ -1,74 +1,75 @@
|
||||
import MockDate from "mockdate";
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
setSystemTime,
|
||||
test,
|
||||
} from "bun:test";
|
||||
import { queryToUserIdentifier, userDiscordIdIsAged } from "./users";
|
||||
|
||||
const QueryToUserIdentifier = suite("queryToUserIdentifier()");
|
||||
const UserDiscordIdIsAged = suite("userDiscordIdIsAged()");
|
||||
|
||||
QueryToUserIdentifier("returns null if no match", () => {
|
||||
assert.equal(queryToUserIdentifier("foo"), null);
|
||||
});
|
||||
|
||||
QueryToUserIdentifier("gets custom url from url", () => {
|
||||
assert.equal(queryToUserIdentifier("https://sendou.ink/u/sendou"), {
|
||||
customUrl: "sendou",
|
||||
describe("queryToUserIdentifier()", () => {
|
||||
test("returns null if no match", () => {
|
||||
expect(queryToUserIdentifier("foo")).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
QueryToUserIdentifier("gets discord id from url", () => {
|
||||
assert.equal(
|
||||
queryToUserIdentifier("https://sendou.ink/u/79237403620945920"),
|
||||
{
|
||||
test("gets custom url from url", () => {
|
||||
expect(queryToUserIdentifier("https://sendou.ink/u/sendou")).toEqual({
|
||||
customUrl: "sendou",
|
||||
});
|
||||
});
|
||||
|
||||
test("gets discord id from url", () => {
|
||||
expect(
|
||||
queryToUserIdentifier("https://sendou.ink/u/79237403620945920"),
|
||||
).toEqual({
|
||||
discordId: "79237403620945920",
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QueryToUserIdentifier("gets custom url from url (without https://)", () => {
|
||||
assert.equal(queryToUserIdentifier("sendou.ink/u/sendou"), {
|
||||
customUrl: "sendou",
|
||||
test("gets custom url from url (without https://)", () => {
|
||||
expect(queryToUserIdentifier("sendou.ink/u/sendou")).toEqual({
|
||||
customUrl: "sendou",
|
||||
});
|
||||
});
|
||||
|
||||
test("gets discord id", () => {
|
||||
expect(queryToUserIdentifier("79237403620945920")).toEqual({
|
||||
discordId: "79237403620945920",
|
||||
});
|
||||
});
|
||||
|
||||
test("gets id", () => {
|
||||
expect(queryToUserIdentifier("1")).toEqual({
|
||||
id: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QueryToUserIdentifier("gets discord id", () => {
|
||||
assert.equal(queryToUserIdentifier("79237403620945920"), {
|
||||
discordId: "79237403620945920",
|
||||
describe("userDiscordIdIsAged()", () => {
|
||||
beforeEach(() => {
|
||||
setSystemTime(new Date("2023-11-25T00:00:00.000Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setSystemTime();
|
||||
});
|
||||
|
||||
test("returns false if discord id is not aged", () => {
|
||||
expect(userDiscordIdIsAged({ discordId: "1177730652641181871" })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("returns true if discord id is aged", () => {
|
||||
expect(userDiscordIdIsAged({ discordId: "79237403620945920" })).toBe(true);
|
||||
});
|
||||
|
||||
test("throws error if discord id missing", () => {
|
||||
expect(() => userDiscordIdIsAged({ discordId: "" })).toThrow();
|
||||
});
|
||||
|
||||
test("throws error if discord id too short", () => {
|
||||
expect(() => userDiscordIdIsAged({ discordId: "1234" })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
QueryToUserIdentifier("gets id", () => {
|
||||
assert.equal(queryToUserIdentifier("1"), {
|
||||
id: 1,
|
||||
});
|
||||
});
|
||||
|
||||
UserDiscordIdIsAged.before.each(() => {
|
||||
MockDate.set(new Date("2023-11-25T00:00:00.000Z"));
|
||||
});
|
||||
|
||||
UserDiscordIdIsAged.after.each(() => {
|
||||
MockDate.reset();
|
||||
});
|
||||
|
||||
UserDiscordIdIsAged("returns false if discord id is not aged", () => {
|
||||
assert.equal(
|
||||
userDiscordIdIsAged({ discordId: "1177730652641181871" }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
UserDiscordIdIsAged("returns true if discord id is aged", () => {
|
||||
assert.equal(userDiscordIdIsAged({ discordId: "79237403620945920" }), true);
|
||||
});
|
||||
|
||||
UserDiscordIdIsAged("throws error if discord id missing", () => {
|
||||
assert.throws(() => userDiscordIdIsAged({ discordId: "" }));
|
||||
});
|
||||
|
||||
UserDiscordIdIsAged("throws error if discord id too short", () => {
|
||||
assert.throws(() => userDiscordIdIsAged({ discordId: "1234" }));
|
||||
});
|
||||
|
||||
QueryToUserIdentifier.run();
|
||||
UserDiscordIdIsAged.run();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user