A Foray into Fantastical Worlds with GPT RPG: Javascript Edition (Part 2)

This article explores the inner workings of GPT RPG, a Javascript game powered by AI. It dives into the frontend code, revealing how components like React and Contexts manage the game state and craft the user experience. You'll discover how the AI interacts with the game and how the UI reflects the ever-changing narrative. The article concludes with exciting ideas for the future of GPT RPG, including multiplayer options and persistent worlds.
Santiago Calvo
March 14, 2024
illustration for outsourcing

A Foray into Fantastical Worlds with GPT RPG: Javascript Edition (Part 2)

Frontend Alchemy: Deep Tech Dive

Welcome to the second part of this adventure! In this post, we’ll take a look at how the code works behind the scenes. In our last post about the MTG Assistant, we delved into the backend structure, and we are reusing the same structure, meaning we are reusing the exact same backend setup for this new GPT agent. Given that we already covered the backend in another post, today we’ll exclusively navigate the frontend labyrinth of GPT RPG, revealing how it shapes the player's journey.

Tech stack

  • Typescript
  • Next.js
  • OpenAi’s Assistant API

GameState context

This file is a central piece of the frontend code for GPT RPG, utilizing React for the UI and a context-based state management system to keep track of the game's data throughout a player's session.

Here's a breakdown of the file contents:

  1. TypeScript Interfaces: These are blueprints for the data structures used throughout the game. They define the shapes of objects like IClass, IAbility, IItem, IPlayer, ICompanion, ICampaign, and IEnemy. Each interface specifies the expected fields and their types, which helps ensure consistency and predictability in the data handled by the game.
  2. IState Type: This defines the shape of the overall state object that includes player information, party details, current enemies, campaign data, and some UI states like isLoading and error. It's a comprehensive state structure that the game will react to.
  3. Initial State: An object that conforms to the IState type and provides initial default values for the game's state.
  4. GameStateContext: This React context is used to provide and consume the state throughout the application. It makes the state accessible to any component in the app without the need to pass props down manually through every level of the component tree.
  5. GameStateProvider: This React component uses the useReducer hook to manage the state based on actions dispatched throughout the application. It provides the GameStateContext to its children, allowing any descendant components to access and modify the game state.
  6. Custom Hook (useGameState): This convenience hook allows you to consume the GameStateContext in functional components easily.

Example GM Response: This commented-out section shows how the game master (GM), powered by GPT, might send an update to the frontend. The update includes narrative content and data changes such as player stats updates, inventory changes, etc.

GameState Reducer

This file represents a crucial part of GPT RPG's state management system. It includes TypeScript definitions for actions and a reducer function to handle state changes within the game. Here's a guided explanation:

  1. Imports and Interface Definitions: The file starts by importing interfaces from the GameStateContext. These interfaces (ICampaign, IClass, ICompanion, IEnemy, IItem, IPlayer, IState) define the shape of various pieces of the game state, such as the player, party members, enemies, and the overall campaign.
  2. IGMResponse Interface: This interface defines the structure of responses from the Game Master (GM), which includes a message and a data object detailing possible updates to the game state. Each field inside the data object is an action that could be dispatched to the reducer function to update the state accordingly.
  3. Action Type Definitions: The Action type is a union of all the possible actions that can be dispatched to the reducer. Each action has a type field that describes what kind of update should be performed and a payload that provides the new data to be merged into the state.
  4. The Reducer Function: This is a standard reducer function following the Redux pattern. It takes the current state and an action and returns a new state based on the action's type. The switch statement handles various action types, such as updating the player's information, adding items to the inventory, adding or removing companions, updating the campaign details, and managing enemies.
  5. State Update Logic: Each case in the reducer's switch statement outlines how the state is updated. For example, the UPDATE_PLAYER action spreads the existing state and overwrites the player field with the new payload. The ADD_ITEM_TO_PLAYER_INVENTORY action spreads the current inventory and adds a new item to it.
  6. Unique Party Update Logic: A notable feature is in the ADD_COMPANION case, where, after adding a new companion, a reduction is performed to ensure that the party list remains unique, preventing duplicates.
  7. Error Handling and Loading State: The reducer also handles setting a loading state (SET_LOADING) and error messages (SET_ERROR), which are critical for giving feedback to the user about the state of the application.

This file encapsulates the logic required to keep the game state consistent and responsive to both player interactions and updates from the AI Game Master. It's a great example of how modern web applications can manage complex states in a scalable and predictable way.

This Chat component is a critical user interface element in GPT RPG that allows players to interact with the AI Game Master. The file showcases how React, custom hooks, and context are used to manage and display messages within the game. Here's an explanation of its main parts:

  1. State and Context: The component uses useState for local state management, such as tracking the new message being typed (newMessage) and whether the application is in a loading state (isLoading). It also uses useAssistant and useGameState to access and manipulate the global state managed by the context providers defined in the respective contexts.
  2. Parsing Assistant Response: The useParseAssistantResponse custom hook is utilized to interpret the response from the AI assistant and dispatch appropriate actions to update the game state accordingly.
  3. sendMessage Function: This async function is the core of the chat interface. When called, it dispatches a loading state, sends the new message along with the current game state to the server via addMessageToThread, and then updates the UI with the response received from the AI.
  4. User Interaction Handling: The handleKeyDown function listens for the Enter key to trigger the sendMessage function, providing a smooth user experience.
  5. Message Management: Messages are displayed in a list, with each message wrapped in an AssistantMessage component. The isNewMessage function is used to determine if a message is the latest user message, which can help in highlighting it in the UI.
  6. Cancellation Functionality: The tryToCancelRun function attempts to cancel the current AI run if needed, which is a safety measure to ensure no pending operations are left if the user decides to stop the interaction.
  7. Effect Hook for Cleanup: The useEffect hook with a return function is used to run the tryToCancelRun function when the component is unmounted, which is a good practice to prevent memory leaks or unwanted operations when the chat component is no longer in use.
  8. UI Structure: The component is structured into various divs, following a typical chat interface layout with a header, a container for messages, and an input area with a send button.
  9. Styling: The styles object imported from chat.module.css provides CSS module-based styling, which is applied to different parts of the component to ensure proper visual representation and user-friendly interaction.

This file, thus, encapsulates the functionality and presentation of the chat interface where players communicate with the AI, manage the game state in real-time, and get updates on the progress of their RPG adventure.

useParseAssistantResponse Custom Hook

The useParseAssistantResponse custom hook plays a pivotal role in GPT RPG by enabling the AI assistant to autonomously modify the game's user interface. It does so by interpreting the assistant's responses and translating them into state updates that reflect across the UI in real-time. Here's an in-depth look at how this hook functions:

  1. Hook Setup: useGameState is leveraged to gain access to the dispatch function, which allows the hook to send actions to the global state based on the assistant's instructions.
  2. Type Guard: The isAction function acts as a safeguard, confirming whether a parsed object qualifies as an actionable item for state updates. This is essential because it ensures that only legitimate and recognized action objects are processed and dispatched.
  3. The Parsing Mechanism: Enclosed within useCallback for optimization, parseAssistantResponse takes a string message from the assistant, parses it into a structured IGMResponse using parseResponseToJSON, and systematically dispatches the resulting actions.
  4. Dispatch Loop: After parsing, it iterates over the data property from the IGMResponse. For each action detected, it dispatches that action, which triggers the appropriate state changes in the game state. This process is how the assistant's decisions and narrative developments get reflected in the game's UI.
  5. Global State Management: Beyond individual actions, it also handles the overall loading state and error messages by dispatching them to the global state, which in turn updates the UI components that display loading indicators or error notifications.
  6. Exception Handling: A robust error handling mechanism captures any issues during parsing and ensures the game state is updated with error information, preventing the application from crashing and maintaining a smooth user experience.

This hook is essentially the bridge between the assistant's narrative engine and the game's state management system, enabling a dynamic and responsive UI that changes according to the unfolding story and the assistant's responses.

The parser function

This code snippet defines the parseResponseToJSON function, which extracts and parses JSON-formatted data from a string response, presumably coming from the AI assistant in GPT RPG. It's a key utility in interpreting the assistant's responses and converting them into a structured format that can be used to update the game state. Here's a breakdown:

  1. Import: The function imports the IGMResponse type, which it uses to type-check the parsed JSON.
  2. Function Definition: parseResponseToJSON is a function that takes a string (response) and returns either an IGMResponse object or null.
  3. Regular Expression: It uses a regular expression to identify a JSON block within the response string. The pattern looks for a JSON string encapsulated by triple backticks, which is a common markdown syntax for code blocks.
  4. Match Checking: If a match is found, it attempts to trim any extra whitespace from the JSON string and parse it using JSON.parse.
  5. Error Handling: The function is wrapped in a try...catch block to handle any parsing errors gracefully. If an error occurs, it logs the error and the faulty JSON string to the console.
  6. Return Value: On successful parsing, it returns the JSON object as an IGMResponse. If parsing is unsuccessful or no JSON block is found, it returns null.

This function is a simple, "naive" implementation for the purpose of the project. It assumes a particular response format and does not handle edge cases or more complex scenarios where the JSON data might not be formatted or encapsulated as expected. It's tailored for a controlled environment where the response format is predictable and consistent. Truth be told, the AI assistant sometimes sends incorrect JSON (it rarely happens in all honestly, but it does happen from time to time), so this approach is not perfect and could use a more robust parsing and fixing mechanism.

The UI Component

The Ui component is a React functional component that serves as the graphical interface for the player’s stats, inventory, party members, enemies, and campaign details within GPT RPG. Here's how it functions and is structured:

  1. State Usage: It uses the useGameState hook to access the global game state, allowing it to display data such as the player's current stats, inventory, and other game-related details.
  2. Inventory Rendering: The renderInventory function takes an array of IItem objects and returns a list of inventory items. Each item is displayed with its name and description within an unordered list.
  3. Health Display: It includes a function getHealthColor to determine the color of the health text based on the player's current health ratio. The renderHealth function uses this to display the player's health in the appropriate color.
  4. UI Sections: The component is divided into several sections, each represented by a <section> element. These sections display different parts of the game state:some text
    • Player Information: Displays the player's name, class, level, experience, health, mana, gold, and inventory.
    • Party Members: Shows a list of current party members, their class, health, mana, and inventory.
    • Current Enemies: Lists the enemies the player is currently facing, along with their health, mana, and abilities.
    • Campaign Details: Provides information about the current campaign, including its name, setting, description, and any additional info.
  5. Health and Stats: Health values for the player, party members, and enemies are displayed using the renderHealth function, which applies color coding for visual aids. Additionally, stats are displayed in a list, providing a quick overview of the character's attributes.
  6. Loading and Error Handling: It includes conditional rendering to show a loading indicator when the state indicates that data is being fetched (isLoading) and to display any errors if they occur (error).
  7. CSS Module Styling: The component uses CSS modules for styling, with class names referenced from styles that are defined in an external CSS file. This approach helps in maintaining a clean separation of concerns and avoiding style conflicts.

This component encapsulates the UI logic of GPT RPG, presenting a real-time view of the game's state to the player in an organized and visually appealing manner. It demonstrates how the dynamic state of a game can be effectively rendered using React's declarative approach, ensuring the UI stays in sync with the underlying data.

Final Thoughts: A Grateful Farewell & Future Horizons

Thank you for joining me on the GPT RPG journey. It's been a delightful learning experience filled with challenges, creativity, and fun. Your interest and time are deeply appreciated. As we look toward the horizon, a few enhancements beckon:

  • Multiplayer Functionality: Envisioning a more interactive and communal experience, utilizing Firestore's snapshot queries could pave the way for real-time multiplayer adventures, bringing together friends and fellow enthusiasts.
  • Persistent Worlds: Implementing a robust database system would allow for saving and restoring player data, ensuring that every hero's journey can continue seamlessly, session after session.
  • Campaign Hub: A feature to showcase current campaigns, allowing players to dive back into their stories or explore new quests at their leisure.

Here's to more adventures and discoveries in the world of coding and storytelling!