File size: 4,230 Bytes
0e23a69 312c390 0e23a69 312c390 0e23a69 312c390 0e23a69 312c390 0e23a69 312c390 0e23a69 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | // ── Game Store (useReducer) ───────────────────────────────────────
// Manages all game state consumed by the UI.
import { useReducer, useCallback } from 'react'
import { apiReset, apiStep } from '../services/api.js'
// ── Initial State ─────────────────────────────────────────────────
const initialState = {
obs: null, // current BoardSimObservation
prevObs: null, // previous obs for delta rendering
done: false,
loading: false,
error: null,
lastReward: null,
lastInfo: null,
rewardTrace: [], // per-step reward history this episode (drives RewardTrace panel)
cumReward: 0, // running total reward this episode
speed: 1.5, // playback speed multiplier
paused: true, // start paused, user clicks Run
seed: 42,
}
// ── Reducer ───────────────────────────────────────────────────────
function reducer(state, action) {
switch (action.type) {
case 'RESET_START':
return { ...initialState, speed: state.speed, loading: true, paused: true }
case 'RESET_SUCCESS':
return {
...state,
loading: false,
obs: action.payload.observation,
prevObs: null,
done: action.payload.done,
lastReward: null,
lastInfo: action.payload.info,
rewardTrace: [],
cumReward: 0,
error: null,
}
case 'STEP_START':
return { ...state, loading: true }
case 'STEP_SUCCESS': {
const r = Number(action.payload.reward ?? 0)
return {
...state,
loading: false,
prevObs: state.obs,
obs: action.payload.observation,
done: action.payload.done,
lastReward: action.payload.reward,
lastInfo: action.payload.info,
rewardTrace: [...state.rewardTrace, r],
cumReward: state.cumReward + r,
error: null,
}
}
case 'SET_SPEED':
return { ...state, speed: action.payload }
case 'TOGGLE_PAUSE':
return { ...state, paused: !state.paused }
case 'SET_PAUSED':
return { ...state, paused: action.payload }
case 'ERROR':
return { ...state, loading: false, error: action.payload, paused: true }
default:
return state
}
}
// ── Hook ──────────────────────────────────────────────────────────
export function useGameStore() {
const [state, dispatch] = useReducer(reducer, initialState)
const resetGame = useCallback(async (seed = 42) => {
dispatch({ type: 'RESET_START' })
try {
const data = await apiReset(seed)
dispatch({ type: 'RESET_SUCCESS', payload: data })
} catch (err) {
dispatch({ type: 'ERROR', payload: err.message })
}
}, [])
const stepGame = useCallback(async (decision, pitch = '') => {
dispatch({ type: 'STEP_START' })
try {
const data = await apiStep(decision, pitch)
dispatch({ type: 'STEP_SUCCESS', payload: data })
if (data.done) dispatch({ type: 'SET_PAUSED', payload: true })
return data
} catch (err) {
dispatch({ type: 'ERROR', payload: err.message })
return null
}
}, [])
const setSpeed = useCallback((v) => dispatch({ type: 'SET_SPEED', payload: v }), [])
const togglePause = useCallback(() => dispatch({ type: 'TOGGLE_PAUSE' }), [])
const setPaused = useCallback((v) => dispatch({ type: 'SET_PAUSED', payload: v }), [])
return { state, resetGame, stepGame, setSpeed, togglePause, setPaused }
}
|