PrimBlocks
A block-stacking game. Arrange falling blocks to create complete lines. How high can you score?
Player Information
Blocks Music
PrimBlocks
Trevor:
Scoreboard:
How to Play
Game Rules
- Create complete horizontal lines
- Lines disappear and grant points
- Game speeds up with level
- Game ends when blocks reach top
Controls
- Arrow keys to move/rotate
- Space for hard drop
- P to pause
- Use on-screen buttons on mobile
How We Built This
Agent Setup
This agent is intended to be one-way audio, meaning that the game is supplying the text, while the agent is supplying audio for the user.
Creating this agent was really simple. Here's the entire code:
1import os
2import json
3from openai import OpenAI
4system_message = """
5You are an in game Commentator named Trevor for Blocks a Tetris-like game that you should not call Tetris.
6The user message will give you information about the player and the way to talk to them.
7Always answer with 1 sentence snappy sound bites related to the stats of the game and adhering to the user's request.
8User Requested Style:
9"""
10async def handler(event, context):
11 client = OpenAI(api_key=context.variables.get("OPENAI_API_KEY"))
12 messages = []
13
14 # Start Message
15 if isinstance(event, StartEvent):
16 intro = "Let's play Prim Blocks!"
17 yield TextToSpeechEvent(intro)
18
19 #Commentary
20 if isinstance(event, TextEvent):
21 parsed_message = json.loads(event.data["text"])
22 style = parsed_message["style"]
23 stats = parsed_message["stats"]
24
25 messages.append({ "role": "system", "content": system_message + style })
26 messages.append({ "role": "user", "content": stats })
27 history = context.get_history()
28 messages.extend(history[-4:])
29
30 if len(messages) > 0:
31 completion = client.chat.completions.create(
32 model="gpt-4o-mini",
33 messages=messages
34 )
35
36 response = completion.choices[0].message.content
37
38 context.add_assistant_message(response)
39
40 yield TextToSpeechEvent(text=response)
Key things to note:
We're just using OpenAI for the intelligence. GPT-4o-mini serves our purposes well. Other than this setup, the key inputs we are going to want from the front end are: style and stats. Style comes directly from a textbox entry from the user. And Stats are output from the game on semi-regular intervals.
Things we hardcoded:
system_message - this could be dynamic from the front end, but for this purpose we kept it static.
Front End
Install Library:
We're building this in React, so we'll install the React library: primvoices-react
In our /primblocks page, we are importing this PrimVoicesProvider, setting up the config, and wrapping our entire page.
Agent Integration
Import
import { PrimVoicesProvider } from 'primvoices-react';
Config - we set our agentID and version.
const primVoicesConfig = {
agentId: "eca55860-5acd-476a-a70a-920d32d755f3",
environment: "production",
}
Wrap - I'm wrapping all the content on the page here.
return (
<PrimVoicesProvider config={primVoicesConfig}>
<main className="relative min-h-screen !pt-[96px] py-8 px-3 sm:py-12 sm:px-4 overflow-hidden">
…
</main>
</PrimVoicesProvider>
);
Agent Passing & Receiving Data
We have a component that is rendered on the /primblocks page called commentator.tsx that is an animatable SVG. Because this component is going to animate to the audio, we're placing the actual calls to the Prim platform as close as possible to it.
Import
import { usePrimVoices } from 'primvoices-react';
onChangeStats
Everytime the stats change, we check to see if audio is playing. If it isn't, we send off another request with sendTextEvent()
passing a JSON string with style and stats.
1const { connect, sendTextEvent, audioStats, isConnected, isPlaying } = usePrimVoices();
2const mouthControls = useAnimation();
3const [mouthOpenness, setMouthOpenness] = useState(0);
4const onChangeStats = async (stats: string) => {
5 console.log("onChangeStats", isConnected);
6 if (!isConnected) {
7 await connect();
8 }
9 if (!isPlaying) {
10 await sendTextEvent(JSON.stringify({ style: messageForTrevor, stats }));
11 }
12};
13// Simple animation cycle for the mouth
14useEffect(() => {
15 if (!audioStats?.isPlayback) {
16 setMouthOpenness(0);
17 } else {
18 // Set the mouth openness with some randomness
19 if (mouthOpenness === 0) {
20 setMouthOpenness(audioStats?.level || 0);
21 } else {
22 setMouthOpenness(0);
23 }
24 }
25}, [audioStats, isPlaying]);
As a bonus, we use the audiostats.level to set the "openness" of the commentator's mouth to add to distraction!
That's it. That's all it took.