Preview
Loading Lively Minds
Portfolio Entertainment System
Web & AI
Lively Minds
Studio:Playpower LabsRole:Web Game Developer
Retro web game where players chat with AI historical figures, one-on-one or in group conversations. Phaser + React client and FastAPI + Gemini backend, built solo.
Tech Stack
- Phaser
- React
- TailwindCSS
- Google Gemini
gallery
what i did
Chapters Read0/6
A retro 2D browser game where players talk with AI-powered historical figures. The gameplay is built in Phaser and embedded in a React app that handles UI and progression.
Key Contributions
- NPC behaviour runs on a modular finite state machine. Every state implements the same
IStatecontract (onEnter,onUpdate,onExit). - A reusable
StateMachinecontroller handles all transitions, so each state has a single job. - Transitions are triggered by events (interaction, timeouts, collisions) defined as enums, instead of chains of conditions, which keeps them predictable and easy for designers to follow.
- Movement, animation and timing live in the states; the core engine doesn't need to know about them.
export interface IState {
stateName: StateEnum;
onEnter(): void;
onUpdate(deltaTime: number): void;
onExit(): void;
}
export enum StateEnum {
IDLE = "idle",
WALK = "walk",
DANCE = "dance",
TALK = "talk",
READ = "read",
WRITE = "write",
}
export enum StateEvent {
WALK_REQUESTED = "walkRequested",
TALK_REQUESTED = "talkRequested",
BOOK_COLLISION = "bookCollision",
WRITE_REQUESTED = "writeRequested",
TIMEOUT = "timeout",
}import { IState } from "./IState";
export class StateMachine<S extends string, E extends string> {
private transitions: Record<S, Partial<Record<E, S>>>;
private states: Map<S, IState>;
private currentState: S | null = null;
constructor(transitions: Record<S, Partial<Record<E, S>>>) {
this.transitions = transitions;
this.states = new Map();
}
addState(state: IState & { stateName: S }): this {
this.states.set(state.stateName, state);
return this;
}
setInitialState(state: S): void {
this.setState(state);
}
private setState(newState: S): void {
if (this.currentState === newState) return;
if (this.currentState) this.states.get(this.currentState)?.onExit();
this.currentState = newState;
this.states.get(this.currentState)?.onEnter();
}
updateState(deltaTime: number): void {
if (!this.currentState) return;
this.states.get(this.currentState)?.onUpdate(deltaTime);
}
handleEvent(event: E): void {
if (!this.currentState) return;
const next = this.transitions[this.currentState]?.[event];
if (!next) return;
if (next !== this.currentState) this.setState(next);
}
getCurrentState(): S | null {
return this.currentState;
}
}- An NPC factory spawns characters from JSON: identity, metadata, sprites and quotes. Adding a character needs no code.
- Random placement with custom collision resolution against other NPCs, the player and the world.
- Fallbacks for missing data or missing sprites.
[
{
"name": "Pythagoras",
"dates": "570–495 BCE",
"about": "Pythagoras was an ancient Greek philosopher and mathematician best known for the Pythagorean theorem.",
"image": "assets/Pythagoras/front_breathe1.png",
"keyIdeas": ["Everything is number"],
"quote": "Number rules the universe."
}
]- Hover highlights, focus targeting and click to interact.
- Name banners and info panels that appear in context.
- Players start conversations straight from the world map.
- Players can eavesdrop on and join ongoing NPC-to-NPC conversations.
- Stateless FastAPI chat service that routes conversations.
- Google Gemini generates historically accurate dialogue.
- Multi-character prompts for group conversations, keeping each NPC's personality distinct.
- The API scales horizontally to serve many game sessions at once.
- One animation layer shared by the player and NPCs, built on an extensible base interface for playback and transitions.
- Supports both flat sprites and layered animations for more complex visuals.
- Kept separate from gameplay and state logic.
- Built the game client and backend solo.
- Connected Phaser gameplay to the React UI through React context and event listeners.
- Prototyped with free assets, then swapped in custom sprite sheets for some of the core characters.