Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
VITE_REPLAY_FILE=replays/test-replay.json
VITE_TENUKI_LOGGER=
VITE_LOG_ANALYTICS=
VITE_LOG_TENUKI=
REPLAY_DOWNLOAD_FOLDER=replays/download
REPLAY_DOWNLOAD_SUB_ID=
REPLAY_DOWNLOAD_COOKIE=
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { createReplayVisualizer, ReplayAdapter } from '@kaggle-environments/core
import { transformer } from './transformers/transformer';
import { getStepRenderTime } from './utils/getStepRenderTime';
import { getStepLabel } from './utils/getStepLabel';
import { GAME_NAME } from './utils/analytics';
import GameRenderer from './components/GameRenderer';
import './App.css';

export default function App() {
const init = useCallback((element: HTMLDivElement) => {
const gameName = 'open_spiel_go';
const gameName = GAME_NAME;
const ui = 'side-panel';
const adapter = new ReplayAdapter({
gameName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTransition } from '../hooks/useReducedMotion';
import useGameStore from '../stores/useGameStore';
import usePreferences from '../stores/usePreferences';
import styles from './Annotation.module.css';
import { trackEvent } from '../utils/analytics';

type Notation = { term: string; title: string; text: string };

Expand Down Expand Up @@ -72,10 +73,10 @@ function resolveAnnotation(
const agent = player.name;

if (moveNumber === 1) {
return { term: 'goes first', title: `${agent} goes first:`, text: 'Unlike Chess, black plays first.' };
return { term: 'black first', title: `${agent} goes first:`, text: 'Unlike Chess, black plays first.' };
}
if (moveNumber === 2) {
return { term: 'komi', title: `${agent} gets Komi:`, text: `A ${komi} point bonus for playing second.` };
return { term: 'white komi', title: `${agent} gets Komi:`, text: `A ${komi} point bonus for playing second.` };
}

const thoughts = player.thoughts?.toLowerCase();
Expand Down Expand Up @@ -118,6 +119,11 @@ export default function Annotation() {
const moveNumber = game.moveNumber();
const notation = showAnnotations && player ? resolveAnnotation(player, moveNumber, game._scorer._komi) : null;

if (notation) {
const trackingTerm = notation.term.replace(/[^a-z\s]/g, '').replace(/\s/g, '-');
trackEvent(`annotation-${trackingTerm}`);
}

return (
<div className={styles.notationSlot} aria-live="polite" ref={inertRef}>
<AnimatePresence mode="wait">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useTransition } from '../hooks/useReducedMotion';
import popoverStyles from './WithPopover.module.css';
import { WithPopover } from './WithPopover.tsx';
import { FeatureToggles } from './FeatureToggles';
import { trackEvent } from '../utils/analytics.ts';
import styles from './BoardControls.module.css';

export default function BoardControls() {
Expand All @@ -25,7 +26,14 @@ export default function BoardControls() {

return (
<div className={styles.boardControls} ref={inertRef}>
<WithPopover id="info" icon="info" label="Game info">
<WithPopover
id="info"
icon="info"
label="Game info"
onChange={(open) => {
trackEvent(`info-${open ? 'open' : 'closed'}`);
}}
>
<p>
Go is an ancient, two-player game in which players try to control more territory on a grid by strategically
placing black and white stones. This game follows Tromp-Taylor rules.
Expand All @@ -35,7 +43,10 @@ export default function BoardControls() {
<input
type="checkbox"
checked={soundEnabled}
onChange={() => toggle('soundEnabled')}
onChange={() => {
toggle('soundEnabled');
trackEvent(`sound-${soundEnabled ? 'off' : 'on'}`);
}}
className={popoverStyles.trigger}
aria-label="Sound"
/>
Expand All @@ -51,7 +62,14 @@ export default function BoardControls() {
<use xlinkHref={`${svgSymbolPath}#sound-off`} />
</svg>
</label>
<WithPopover id="settings" icon="settings" label="Settings">
<WithPopover
id="settings"
icon="settings"
label="Settings"
onChange={(open) => {
trackEvent(`settings-${open ? 'open' : 'closed'}`);
}}
>
<FeatureToggles />
</WithPopover>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import usePreferences from '../stores/usePreferences';
import styles from './FeatureToggles.module.css';
import { trackEvent } from '../utils/analytics';

export function FeatureToggles() {
const { toggle, showHeroAnimations, showTerritory, showAnnotations, reducedMotion } = usePreferences();
Expand All @@ -14,7 +15,10 @@ export function FeatureToggles() {
type="checkbox"
className={styles.switch}
checked={showTerritory}
onChange={() => toggle('showTerritory')}
onChange={() => {
toggle('showTerritory');
trackEvent(`settings-live-territory-${showTerritory ? 'off' : 'on'}`);
}}
/>
</label>
</li>
Expand All @@ -25,7 +29,10 @@ export function FeatureToggles() {
type="checkbox"
className={styles.switch}
checked={showHeroAnimations}
onChange={() => toggle('showHeroAnimations')}
onChange={() => {
toggle('showHeroAnimations');
trackEvent(`settings-pop-up-animations-${showHeroAnimations ? 'off' : 'on'}`);
}}
/>
</label>
</li>
Expand All @@ -36,7 +43,10 @@ export function FeatureToggles() {
type="checkbox"
className={styles.switch}
checked={reducedMotion}
onChange={() => toggle('reducedMotion')}
onChange={() => {
toggle('reducedMotion');
trackEvent(`settings-reduce-motion-${reducedMotion ? 'off' : 'on'}`);
}}
/>
</label>
</li>
Expand All @@ -47,7 +57,10 @@ export function FeatureToggles() {
type="checkbox"
className={styles.switch}
checked={showAnnotations}
onChange={() => toggle('showAnnotations')}
onChange={() => {
toggle('showAnnotations');
trackEvent(`settings-board-annotations-${showAnnotations ? 'off' : 'on'}`);
}}
/>
</label>
</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react';
import { Ribbon } from './Ribbon';
import useGameStore from '../stores/useGameStore';
import { trackEvent } from '../utils/analytics';
import styles from './GameOver.module.css';

function formatDuration(totalSeconds: number): string {
Expand Down Expand Up @@ -107,6 +108,8 @@ export default function GameOver() {
},
];

trackEvent('game-over');

return (
<dialog ref={dialogRef} className={styles.modal} aria-label="Game over" tabIndex={-1}>
<div className="ribbon">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { HeroTypes, detectHeroType } from '../utils/heroTypes';
import { RivePopover } from './RivePopover';
import useGameStore from '../stores/useGameStore';
import usePreferences from '../stores/usePreferences';
import { trackEvent } from '../utils/analytics';

interface Hero {
src: string;
Expand Down Expand Up @@ -41,27 +42,32 @@ export default function HeroAnimation() {
const opponent = color === 'black' ? 'White' : 'Black';
const captures = state.capturedPositions?.length;

let src, text;
let src, text, event;
switch (heroType) {
case HeroTypes.PASS:
src = passRiv;
text = `${player} passes the turn.`;
event = 'pass';
break;
case HeroTypes.DOUBLE_PASS:
src = doublePassRiv;
text = 'Double Pass: game over.';
event = 'double-pass';
break;
case HeroTypes.FIRST_CAPTURE:
src = firstCaptureRiv;
text = `${player} captures first.`;
event = 'first-capture';
break;
case HeroTypes.CRITICAL_HIT:
src = criticalHitRiv;
text = `${player} captures ${captures} stones.`;
event = 'critical-hit';
break;
case HeroTypes.DRAGON_LOSS:
src = dragonLossRiv;
text = `${opponent} loses a dragon.`;
event = 'dragon-loss';
break;
default:
return;
Expand All @@ -70,13 +76,14 @@ export default function HeroAnimation() {
// Let the board play out before showing the Rive animation.
const timeout = setTimeout(() => {
setHero({ src, text, color, step });
if (!game.gameOver) trackEvent(`pop-up-animation-${event}`);
}, 600);

return () => {
clearTimeout(timeout);
setHero(null);
};
}, [game]);
}, [game, showHeroAnimations]);

const isVisible = !!hero && !game.gameOver;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Ribbon } from './Ribbon';
import useGameStore from '../stores/useGameStore';
import usePreloader from '../stores/usePreloader';
import { useTransition } from '../hooks/useReducedMotion';
import { trackEvent } from '../utils/analytics';
import styles from './VersusBanner.module.css';

export default function VersusBanner() {
Expand All @@ -11,6 +12,8 @@ export default function VersusBanner() {
const enterTransition = useTransition({ duration: 1.3, ease: [0.22, 1.15, 0.36, 1] });
const exitTransition = useTransition({ duration: 0.3 });

if (loaded && game.gameStart) trackEvent('versus-banner');

return (
<AnimatePresence>
{loaded && game.gameStart && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,23 @@ interface Props {
icon: string;
id: string;
label: string;
onChange?: (open: boolean) => void;
}

export function WithPopover({ children, icon, id, label }: Props) {
export function WithPopover({ children, icon, id, label, onChange }: Props) {
const [open, setOpen] = useState(false);
const onChangeRef = useRef(open);
const transition = useTransition({ duration: 0.2, ease: 'easeOut' });
const iconPath = `${svgSymbolPath}#${icon}`;
const triggerRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (onChange && onChangeRef.current !== open) {
onChangeRef.current = open;
onChange(open);
}

if (!open) return;

// Wait a frame, then focus the popover.
Expand Down Expand Up @@ -47,7 +54,7 @@ export function WithPopover({ children, icon, id, label }: Props) {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [open]);
}, [open, onChange]);

return (
<div className={styles.wrapper}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { postAnalyticsEvent } from '@kaggle-environments/core';

export const GAME_NAME = 'open_spiel_go';

export function trackEvent(event: string) {
if (import.meta.env.DEV && import.meta.env.VITE_LOG_ANALYTICS) {
console.log(`Track Event: ${event}`);
}

postAnalyticsEvent({ game: GAME_NAME, action: event });
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Game } from 'tenuki';

export function tenukiLogger(game: Game) {
if (import.meta.env.DEV === false || !import.meta.env.VITE_TENUKI_LOGGER) return;
if (!import.meta.env.DEV || !import.meta.env.VITE_LOG_TENUKI) return;

const state = game.currentState();
const size = game.boardSize;
Expand Down