Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c8166cf
games: Start adding Splendor assets
PartMan7 Aug 1, 2025
6c554ea
games: Add more Splendor trainer metadata
PartMan7 Aug 1, 2025
2e5bc72
games: Rename Chili/Cilan/Cress to Red
PartMan7 Aug 3, 2025
1a00d73
games: Added all trainer sprites for Splendor
PartMan7 Aug 3, 2025
63e029d
games: Added all type sprites for Splendor
PartMan7 Aug 3, 2025
9cf2094
games: Fix startCount for tokens
PartMan7 Aug 3, 2025
4adaa8a
games: Add colorless Pokémon sprites and full Splendor metadata
PartMan7 Aug 3, 2025
2404ba6
games: Add most Splendor Pokemon images
Audiino Aug 4, 2025
726e627
games: Add Chansey to Splendor Pokemon
Audiino Aug 4, 2025
22b7011
chore: Rename asset files for Alolans
PartMan7 Aug 4, 2025
5bc0ceb
games: Start setting up Splendor logic
PartMan7 Aug 4, 2025
669dc01
games: Add a debug mode, move assets for Splendor, and assorted rende…
PartMan7 Aug 4, 2025
db9006f
games: Support flipped, stacked, and placeholder cards for Splendor
PartMan7 Aug 4, 2025
3f839c5
games: Add trainer cards
PartMan7 Aug 5, 2025
d86b4f6
games: Design changes for reserved cards
PartMan7 Aug 5, 2025
f520faf
games: Make cards clickable as needed
PartMan7 Aug 5, 2025
24333f4
games: Add player/board renderers for Splendor and try to start clean…
PartMan7 Aug 6, 2025
8885a0c
games: Add render capable of using renderCtx
PartMan7 Aug 6, 2025
cae5cca
games: Add most of the game logic
PartMan7 Aug 7, 2025
a6c635f
games: TokenInputs added for Splendor and state handling for TOO_MANY…
PartMan7 Aug 7, 2025
2f58cb0
games: Add WildCardInput for Splendor
PartMan7 Aug 7, 2025
0210848
games: Splendor discounts and assorted bugfixes
PartMan7 Aug 8, 2025
6246244
games: Chatlogs and always-visible context for Splendor
PartMan7 Aug 8, 2025
d0e9948
games: Swap Grovyle and Eldegoss for Splendor
PartMan7 Aug 8, 2025
4ad6eee
games: Add Foongus, Frillish, Litwick, Scovillain, Swadloon to Splendor
Audiino Aug 8, 2025
60dcef0
games: Add Grovyle to Splendor, update Decidueye and Frillish
Audiino Aug 9, 2025
a74a0cc
games: Add Nuzleaf and Scorbunny to Splendor
Audiino Aug 9, 2025
e11fc7a
games: Add Typhlosion Hisui and Wo-Chien to Splendor
Audiino Aug 11, 2025
15d8edb
games: Add Chandelure, Eldegoss, Mudkip to Splendor
Audiino Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
739 changes: 61 additions & 678 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "A chatbot for Pokémon Showdown",
"main": "src/index.js",
"scripts": {
"debug": "./scripts/debug-games/start.sh",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"notify-unpushed": "sh scripts/notify_unpushed.sh",
Expand Down Expand Up @@ -47,6 +48,7 @@
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/livereload": "^0.9.5",
"@types/node": "^20.4.4",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
Expand All @@ -58,6 +60,7 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"husky": "^8.0.3",
"livereload": "^0.9.3",
"prettier": "^3.3.3"
}
}
1 change: 1 addition & 0 deletions scripts/debug-games/live/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.html
5 changes: 5 additions & 0 deletions scripts/debug-games/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

npx http-server src/web -p 8080 &
USE_WEB=false WEB_PORT=8080 WEB_URL=http://localhost:8080 npx ts-node --project tsconfig.debug.json src/ps/games/debug.ts &
npx http-server scripts/debug-games/live -p 8081 -o page.html
73 changes: 73 additions & 0 deletions scripts/debug-games/templates/page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Size: Desktop</title>
<style>
body {
/* Art by Daniel Kong */
background: url('https://play.pokemonshowdown.com/fx/client-bg-shaymin.jpg') left center / cover no-repeat fixed
rgb(84, 107, 172);
}
#page {
border: 1px solid black;
padding: 12px;
background: #000a;
color: white;
border-radius: 12px;
}

#page.desktop {
width: 600px;
max-width: 600px;
}
#page.mobile {
width: 480px;
max-width: 480px;
overflow: auto;
}
#page.ruka {
width: 360px;
max-width: 360px;
max-height: 540px;
overflow: auto;
}
</style>
<script src="http://localhost:8082/livereload.js?snipver=1"></script>
</head>
<body>
<center>
<div style="display: flex; gap: 4px; justify-content: center; margin: 12px">
<button id="button-desktop" onclick="setView('desktop')">Desktop</button>
<button id="button-mobile" onclick="setView('mobile')">Mobile</button>
<button id="button-ruka" onclick="setView('ruka')">Ruka</button>
</div>
<center id="page">{HTML}</center>
</center>
<script>
/** @typedef {'desktop' | 'mobile' | 'ruka'} View */
/** @type View */
let view;

/** @type {Record<View, number>} */
const widths = { desktop: 600, mobile: 480, ruka: 360 };

/** @param {View} newView */
function setView(newView) {
view = newView;
localStorage.setItem('view', newView);

const page = document.getElementById('page');
page.classList.remove('desktop', 'mobile', 'ruka');
page.classList.add(newView);

document.querySelectorAll('button[id^=button-]').forEach(button => {
if (button.id === `button-${newView}`) button.setAttribute('disabled', 'true');
else button.removeAttribute('disabled');
});
}

setView(localStorage.getItem('view') || 'desktop');
</script>
</body>
</html>
1 change: 1 addition & 0 deletions src/globals/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import '@/globals/augment';
import '@/globals/patches';
import '@/globals/prototypes';
34 changes: 31 additions & 3 deletions src/globals/prototypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ declare global {
count(): Record<T & (string | number), number>;
count(map: true): Map<T, number>;
group(size: number): T[][];
groupBy<Key extends string>(classification: (element: T) => Key): Partial<Record<Key, T[]>>;
/**
* filterMap runs map. Only results that are NOT exactly 'undefined' are returned.
*/
filterMap<X>(cb: (element: T, index: number, thisArray: T[]) => X | undefined): X[];
list($T?: TranslationFn | string): string;
random(rng?: RNGSource): T | null;
Expand All @@ -19,26 +23,37 @@ declare global {
/** Default order is ascending */
sortBy(getSort: ((term: T, thisArray: T[]) => unknown) | null, dir?: 'asc' | 'desc'): T[];
space<S = unknown>(spacer: S): (T | S)[];
sum(): T;
sum(): T extends number ? number : never;
unique(): T[];
}
interface ReadonlyArray<T> {
access<V = ArrayAtom<T>>(pos: number[]): V;
count(): Record<T & (string | number), number>;
count(map: true): Map<T, number>;
/**
* filterMap runs map. Only results that are NOT exactly 'undefined' are returned.
*/
filterMap<X>(cb: (element: T, index: number, thisArray: T[]) => X | undefined): X[];
group(size: number): T[][];
groupBy<Key extends string>(classification: (element: T) => Key): Partial<Record<Key, T[]>>;
list($T?: TranslationFn): string;
random(rng?: RNGSource): T | null;
sample(amount: number, rng?: RNGSource): T[];
space<S = unknown>(spacer: S): (T | S)[];
sum(): T;
sum(): T extends number ? number : never;
unique(): T[];
}

interface String {
gsub(match: RegExp, replace: string | ((arg: string, ...captures: string[]) => string)): string;
lazySplit(match: string | RegExp, cases: number): string[];

/**
* Split the string exactly as many times as needed.
* @param matcher Pattern/string to split by.
* @param cases Number of cases to split.
* @example 'a b c'.lazySplit(' ', 1); // ['a', 'bc']
*/
lazySplit(matcher: string | RegExp, cases: number): string[];
}

interface Number {
Expand Down Expand Up @@ -119,6 +134,19 @@ Object.defineProperties(Array.prototype, {
return [];
},
},
groupBy: {
enumerable: false,
writable: false,
configurable: false,
value: function <T, Key extends string>(this: T[], classification: (element: T) => Key): Partial<Record<Key, T[]>> {
return this.reduce<Partial<Record<Key, T[]>>>((acc, current) => {
const key = classification(current);
if (acc[key]) acc[key].push(current);
else acc[key] = [current];
return acc;
}, {});
},
},
list: {
enumerable: false,
writable: false,
Expand Down
4 changes: 2 additions & 2 deletions src/ps/games/battleship/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class Battleship extends BaseGame<State> {
this.room.sendHTML(...renderMove(logEntry, this));
if (this.state.ready.A === true && this.state.ready.B === true) {
this.state.allReady = true;
this.nextPlayer();
this.endTurn();
} else {
this.update(player.id);
}
Expand Down Expand Up @@ -122,7 +122,7 @@ export class Battleship extends BaseGame<State> {
this.winCtx = { type: 'win', winner: player, loser: this.players[opponent] };
this.end();
}
this.nextPlayer();
this.endTurn();
this.update();
break;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ps/games/chess/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class Chess extends BaseGame<State> {
this.cleanup();
this.state.pgn = this.lib.pgn();

this.nextPlayer();
this.endTurn();
}

// Cleans up stuff like selections and draw offers
Expand Down
2 changes: 1 addition & 1 deletion src/ps/games/connectfour/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class ConnectFour extends BaseGame<State> {
this.end();
return true;
}
this.nextPlayer();
this.endTurn();
return board;
}

Expand Down
40 changes: 40 additions & 0 deletions src/ps/games/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable no-console -- This isn't part of the bot */
import 'dotenv/config';

import { watch } from 'chokidar';
import { promises as fs } from 'fs';
import { createServer } from 'livereload';
import path from 'path';

import { debounce } from '@/utils/debounce';
import { fsPath } from '@/utils/fsPath';

const debugDir = fsPath('..', 'scripts', 'debug-games');

async function baseRenderTemplates(): Promise<void> {
const { test } = await import('@/ps/games/test');
const html = await test();
console.log('Rendering templates!');
fs.readdir(path.join(debugDir, 'templates'))
.then(files => files.filter(file => file.endsWith('.html')))
.then(templates =>
Promise.all(
templates.map(async templateName => {
const template = await fs.readFile(path.join(debugDir, 'templates', templateName), 'utf8');
await fs.writeFile(path.join(debugDir, 'live', templateName), template.replace('{HTML}', html));
})
)
)
.then(() => console.log('Rendered templates.'));
}

const renderTemplates = debounce(baseRenderTemplates, 100);

if (require.main === module) {
const watcher = watch([path.join(debugDir, 'templates'), fsPath('ps', 'games')]);
watcher.on('all', async () => {
renderTemplates();
});
const server = createServer({ port: 8082 });
server.watch(path.join(debugDir, 'live'));
}
18 changes: 12 additions & 6 deletions src/ps/games/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class BaseGame<State extends BaseState> {

onAddPlayer?(user: User, ctx: string): ActionResponse;
onAfterAddPlayer?(player: Player): void;
onLeavePlayer?(player: Player, ctx: string | User): ActionResponse;
onLeavePlayer?(player: Player, ctx: string | User): ActionResponse<'end' | null>;
onForfeitPlayer?(player: Player, ctx: string | User): ActionResponse;
onReplacePlayer?(turn: BaseState['turn'], withPlayer: User): ActionResponse;
onAfterReplacePlayer?(player: Player): void;
Expand Down Expand Up @@ -247,6 +247,7 @@ export class BaseGame<State extends BaseState> {
}
backup(): void {
if (this.meta.players === 'single') return; // Don't back up single-player games
if (this.endedAt) return;
const backup = this.serialize();
gameCache.set({ id: this.id, room: this.roomid, game: this.meta.id, backup, at: Date.now() });
}
Expand Down Expand Up @@ -345,7 +346,7 @@ export class BaseGame<State extends BaseState> {
cb: () => {
const playersLeft = Object.values(this.players).filter((player: Player) => !player.out);
if (playersLeft.length <= 1) this.end('dq');
else if (this.turn === player.turn) this.nextPlayer(); // Needs to be run AFTER consumer has finished DQing
else if (this.turn === player.turn) this.endTurn(); // Needs to be run AFTER consumer has finished DQing
this.backup();
},
},
Expand All @@ -356,7 +357,12 @@ export class BaseGame<State extends BaseState> {
delete this.players[player.turn];
return {
success: true,
data: { message: this.$T(staffAction ? 'GAME.REMOVED' : 'GAME.LEFT', { player: player.name }) },
data: {
message: this.$T(staffAction ? 'GAME.REMOVED' : 'GAME.LEFT', { player: player.name }),
cb: () => {
if (removePlayer?.data === 'end') this.end('dq');
},
},
};
}

Expand All @@ -377,7 +383,7 @@ export class BaseGame<State extends BaseState> {
this.players[newTurn] = { ...oldPlayer, ...assign, turn: newTurn };
if (!this.meta.turns) this.turns.splice(this.turns.indexOf(turn), 1, newTurn);
if (this.turn === turn) this.turn = newTurn;
this.spectators.remove(oldPlayer.id);
this.spectators.remove(oldPlayer.id, withPlayer.id);
this.onAfterReplacePlayer?.(this.players[newTurn]);
this.backup();
return { success: true, data: this.$T('GAME.SUB', { in: withPlayer.name, out: oldPlayer.name }) };
Expand Down Expand Up @@ -410,7 +416,7 @@ export class BaseGame<State extends BaseState> {
if (tryStart?.success === false) return tryStart;
this.started = true;
if (!this.turns.length) this.turns = Object.keys(this.players).shuffle(this.prng);
this.nextPlayer();
this.endTurn();
this.startedAt = new Date();
this.setTimer('Game started');
this.onAfterStart?.();
Expand All @@ -425,7 +431,7 @@ export class BaseGame<State extends BaseState> {
}

// Increments turn as needed and backs up state.
nextPlayer(): State['turn'] | null {
endTurn(): State['turn'] | null {
let current = this.turn;
do {
current = this.getNext(current);
Expand Down
5 changes: 5 additions & 0 deletions src/ps/games/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Mastermind, meta as MastermindMeta } from '@/ps/games/mastermind';
import { Othello, meta as OthelloMeta } from '@/ps/games/othello';
import { Scrabble, meta as ScrabbleMeta } from '@/ps/games/scrabble';
import { SnakesLadders, meta as SnakesLaddersMeta } from '@/ps/games/snakesladders';
import { Splendor, meta as SplendorMeta } from '@/ps/games/splendor';
import { GamesList, type Meta } from '@/ps/games/types';

export const Games = {
Expand Down Expand Up @@ -41,5 +42,9 @@ export const Games = {
meta: SnakesLaddersMeta,
instance: SnakesLadders,
},
[GamesList.Splendor]: {
meta: SplendorMeta,
instance: Splendor,
},
} satisfies Readonly<Record<GamesList, Readonly<{ meta: Meta; instance: unknown }>>>;
export type Games = typeof Games;
2 changes: 1 addition & 1 deletion src/ps/games/lightsout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class LightsOut extends BaseGame<State> {
if (this.state.board.every(row => row.every(cell => cell === false))) {
return this.end();
}
this.nextPlayer();
this.endTurn();
}

onEnd(type: Exclude<EndType, 'loss'>): TranslatedText {
Expand Down
2 changes: 1 addition & 1 deletion src/ps/games/mastermind/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class Mastermind extends BaseGame<State> {
if (this.state.board.length >= this.state.cap) {
return this.end('loss');
}
this.nextPlayer();
this.endTurn();
}
guess(guess: Guess): GuessResult {
if (this.state.board.length === 0 && !this.setBy) this.closeSignups();
Expand Down
2 changes: 1 addition & 1 deletion src/ps/games/othello/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class Othello extends BaseGame<State> {
board[i][j] = turn;
this.log.push({ action: 'play', time: new Date(), turn, ctx: [i, j] });

const next = this.nextPlayer();
const next = this.endTurn();
if (!next) this.end();
return board;
}
Expand Down
6 changes: 3 additions & 3 deletions src/ps/games/scrabble/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export class Scrabble extends BaseGame<State> {
this.passCount = 0;

if (rack.length === 0) return this.end();
const next = this.nextPlayer();
const next = this.endTurn();
if (!next) return this.end();
}

Expand Down Expand Up @@ -292,7 +292,7 @@ export class Scrabble extends BaseGame<State> {
this.log.push(logEntry);
this.room.sendHTML(...renderMove(logEntry, this));

this.nextPlayer();
this.endTurn();
}

pass(): void {
Expand All @@ -305,7 +305,7 @@ export class Scrabble extends BaseGame<State> {
if (this.passCount > Object.keys(this.players).length) {
return this.end('regular');
}
this.nextPlayer();
this.endTurn();
}

onEnd(type?: EndType): TranslatedText {
Expand Down
Loading
Loading