Skip to content

Commit bd915cc

Browse files
committed
[Werewolf] - update legacy-adapter and player to work with Werewolf
I'm not crazy about the way we are injecting the unstable_playerControls (previously __mainContext) - it would be nice to learn what exactly werewolf needs and see if we could work around it with transformers or more targeted exposed actions Also updated the way that our dependencies use @kaggle-environments/core to make the development experience a bit more streamlined
1 parent 184558d commit bd915cc

File tree

10 files changed

+178
-70
lines changed

10 files changed

+178
-70
lines changed

kaggle_environments/envs/open_spiel_env/games/chess/visualizer/default/src/chess_renderer.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { ChessStep } from '@kaggle-environments/core';
22
import { DARK_SQUARE_COLOR, DEFAULT_NUM_COLS, DEFAULT_NUM_ROWS, LIGHT_SQUARE_COLOR, PIECE_IMAGES_SRC } from './consts';
3-
import { RendererOptions } from './main';
43

5-
export function renderer(options: RendererOptions) {
4+
export function renderer(options: any) {
65
const { steps, step, parent, playerNames, width = 400, height = 400, viewer } = options;
76

87
let currentBoardElement: HTMLElement | null = null;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"typescript": "^5.9.3",
2020
"typescript-eslint": "^8.46.1",
2121
"vite": "^5.0.0",
22-
"vite-plugin-checker": "^0.11.0"
22+
"vite-plugin-checker": "^0.11.0",
23+
"vite-tsconfig-paths": "^5.1.4"
2324
}
2425
}

pnpm-lock.yaml

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/core/src/adapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { ReplayData } from "./types";
1+
import { ReplayData } from './types';
22

33
export interface GameAdapter {
44
mount(container: HTMLElement, initialData: ReplayData): void;
5-
render(step: number, replay: ReplayData, agents: any[]): void;
5+
render(step: number, replay: ReplayData, agents: any[], replayerInstance?: any): void;
66
unmount(): void;
77
}

web/core/src/legacy-adapter.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type LegacyRenderer = (options: any, container?: HTMLElement) => void;
88
export class LegacyAdapter implements GameAdapter {
99
private container: HTMLElement | null = null;
1010
private renderer: LegacyRenderer;
11+
private isInitialRender = true;
1112

1213
constructor(renderer: LegacyRenderer) {
1314
this.renderer = renderer;
@@ -17,11 +18,32 @@ export class LegacyAdapter implements GameAdapter {
1718
this.container = container;
1819
}
1920

20-
render(step: number, replay: ReplayData, agents: any[]): void {
21+
// replayerInstance passing is a bit of a hack for werewolf - would be nice to eliminate it
22+
render(step: number, replay: ReplayData, agents: any[], replayerInstance?: any): void {
2123
if (!this.container) return;
2224

23-
// Clear container before rendering, as legacy renderers often append.
24-
this.container.innerHTML = '';
25+
// Clear container only on the first render pass.
26+
if (this.isInitialRender) {
27+
this.container.innerHTML = '';
28+
this.isInitialRender = false;
29+
}
30+
31+
// Generally it would be better to not do this - but werewolf needs some inner frame control
32+
// that conforms to this interface - marking as unstable for now to see if we can figure out
33+
// a better long-term solution here
34+
const unstable_replayerControls = replayerInstance
35+
? {
36+
setStep: (newStep: number) => replayerInstance.setStep(newStep),
37+
play: (continuing?: boolean) => replayerInstance.play(continuing),
38+
pause: () => replayerInstance.pause(),
39+
setPlaying: (playing: boolean) => replayerInstance.setPlayingState(playing),
40+
// Expose current step and playing state for renderer to read
41+
step: replayerInstance.step,
42+
playing: replayerInstance.playing,
43+
// Expose the actual player object for advanced scenarios if needed
44+
_replayerInstance: replayerInstance,
45+
}
46+
: undefined;
2547

2648
const renderOptions = {
2749
// For chess/poker
@@ -37,6 +59,8 @@ export class LegacyAdapter implements GameAdapter {
3759
step: step,
3860
width: this.container.clientWidth,
3961
height: this.container.clientHeight,
62+
63+
unstable_replayerControls: unstable_replayerControls,
4064
};
4165

4266
// Some legacy renderers take the container as a second argument.

web/core/src/player.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,13 @@ export class ReplayVisualizer {
218218
needsRender = true;
219219
}
220220

221-
// Update steps from 'setSteps'
221+
// Update steps from 'setSteps' - this is a special case.
222+
// The renderer is taking control of the steps, and we just need to update
223+
// our controls to match, not trigger a destructive re-render.
222224
if (event.data.setSteps && this.replay) {
223225
this.replay.steps = event.data.setSteps;
224-
needsRender = true;
226+
this.stepSlider.max = (this.replay.steps.length > 0 ? this.replay.steps.length - 1 : 0).toString();
227+
this.renderControls();
225228
}
226229

227230
// Overwrite replay object if a full 'replay' is provided
@@ -285,7 +288,7 @@ export class ReplayVisualizer {
285288

286289
// Only render/tick if not told to skip (e.g., during HMR restore)
287290
if (!options.skipRender) {
288-
this.adapter.render(this.step, this.replay, this.agents);
291+
this.adapter.render(this.step, this.replay, this.agents, this);
289292
this.tick();
290293
}
291294
}
@@ -300,7 +303,7 @@ export class ReplayVisualizer {
300303
}
301304
// --- End HMR Logic ---
302305

303-
this.adapter.render(this.step, this.replay, this.agents);
306+
this.adapter.render(this.step, this.replay, this.agents, this);
304307
this.renderControls();
305308
}
306309

@@ -334,6 +337,18 @@ export class ReplayVisualizer {
334337
this.renderControls();
335338
}
336339

340+
/**
341+
* Sets the playing state directly and updates controls, but does not trigger
342+
* the timer-based 'tick()' method. Useful for renderers that implement their
343+
* own playback logic (e.g., audio-driven).
344+
* @param playing The new playing state.
345+
*/
346+
public setPlayingState(playing: boolean) {
347+
if (this.playing === playing) return;
348+
this.playing = playing;
349+
this.renderControls();
350+
}
351+
337352
private tick = () => {
338353
if (!this.playing || !this.replay) return;
339354

web/core/src/preact-adapter.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,32 @@ import { GameAdapter } from './adapter';
33
import { ReplayData } from './types';
44

55
interface RendererProps {
6-
replay: ReplayData;
7-
step: number;
8-
agents: any[];
6+
replay: ReplayData;
7+
step: number;
8+
agents: any[];
99
}
1010

1111
export class PreactAdapter implements GameAdapter {
12-
private container: HTMLElement | null = null;
13-
private renderer: FunctionComponent<RendererProps>;
12+
private container: HTMLElement | null = null;
13+
private renderer: FunctionComponent<RendererProps>;
1414

15-
constructor(renderer: FunctionComponent<RendererProps>) {
16-
this.renderer = renderer;
17-
}
15+
constructor(renderer: FunctionComponent<RendererProps>) {
16+
this.renderer = renderer;
17+
}
1818

19-
mount(container: HTMLElement, initialData: ReplayData): void {
20-
this.container = container;
21-
this.render(0, initialData, []);
22-
}
19+
mount(container: HTMLElement, initialData: ReplayData): void {
20+
this.container = container;
21+
this.render(0, initialData, []);
22+
}
2323

24-
render(step: number, replay: ReplayData, agents: any[]): void {
25-
if (!this.container) return;
26-
render(h(this.renderer, { replay, step, agents }), this.container);
27-
}
24+
render(step: number, replay: ReplayData, agents: any[]): void {
25+
if (!this.container) return;
26+
render(h(this.renderer, { replay, step, agents }), this.container);
27+
}
2828

29-
unmount(): void {
30-
if (!this.container) return;
31-
render(null, this.container);
32-
this.container = null;
33-
}
29+
unmount(): void {
30+
if (!this.container) return;
31+
render(null, this.container);
32+
this.container = null;
33+
}
3434
}

web/core/vite.config.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { defineConfig } from "vite";
2-
import { resolve } from "path";
3-
import dts from "vite-plugin-dts";
1+
import { defineConfig } from 'vite';
2+
import { resolve } from 'path';
3+
import dts from 'vite-plugin-dts';
44

55
// https://vitejs.dev/config/
66
export default defineConfig({
77
plugins: [dts()],
88
build: {
99
lib: {
10-
entry: resolve(__dirname, "src/index.ts"),
11-
name: "@kaggle-environments/core",
12-
fileName: "index"
10+
entry: resolve(__dirname, 'src/index.ts'),
11+
name: '@kaggle-environments/core',
12+
fileName: 'index',
1313
},
14-
outDir: "dist",
14+
outDir: 'dist',
1515
rollupOptions: {
16-
external: ["preact", "preact/hooks", "htm"]
17-
}
18-
}
16+
external: ['preact', 'preact/hooks', 'htm'],
17+
},
18+
},
1919
});

web/tsconfig.base.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
"skipLibCheck": true,
1818
"jsx": "preserve",
1919
"jsxImportSource": "preact",
20-
"types": ["vite/client"]
20+
"types": ["vite/client"],
21+
"baseUrl": ".",
22+
"paths": {
23+
"@kaggle-environments/core": ["core/src/index.ts"]
24+
}
2125
}
2226
}

0 commit comments

Comments
 (0)