He estado pensando en hacer una plataforma web de RPG multijugador desde mis días como desarrollador de Flash.Hay muchas opciones de TTRPG en línea (muchas buenas!) pero ninguna de ellas se sentía muy bien para cómo me gustaría jugar.
¿Has vistoEl videodonde Deborah Ann Woll muestra a Jon Bernthal cómo jugar a D&D? (tiene 2M vistas!)
¿Y si pudiera hacer algo que se sienta así?
La idea
Una plataforma de RPG multijugador en línea basada en la narración, ligera en las reglas pero alta en la narración compartida, con resultados determinados por el rollo de un D20.
Siempre he amadoMódulos D&DLos encuentros detallados y la historia épica planes para los jugadores para descubrir a medida que sus personajes hacen su camino a través de una final climática.
¿Y si pudiera crear una forma para que las personas crearan sus propios módulos de aventura, para cualquier género de RPG, y luego dejaran que los jugadores ejecuten a sus personajes a través de esas aventuras?
¿Quién iba a ejecutar los juegos?Los maestros de juegos son difíciles de encontrar.¿Y si pudiese entrenar a un AI para ser un maestro de juegos?Para ejecutar realmente un plan de aventura bien estructurado (diseñado por un humano!) que sería divertido para los jugadores y no solo un montón deEl Slop?
Empezando a
He estado disfrutando de construir muchas cosas con AI (verMi proyecto comienzaVoy a usar mi pila preferida para hacerD20Adventures.com, una aplicación web Next.js con Tailwind para la interfaz de usuario (need it even be said?) alimentado por laEl SDKEl uso de Gemini y aBase de datos convexaDesarrollado en Vercel.
Y he decidido construirlo en el abierto, publicando el códigoEn GitHub.
El prototipo
Para un prototipo, estoy literalmente comenzando con el escenario expuesto en el podcast, un ranger que camina por los bosques por la noche oye una grieta en la distancia, que resulta ser un oso.
Mi objetivo es construir una breve aventura de un solo tiro y ver si puedo entrenar a un DM de IA para ejecutar realmente una pequeña serie de turnos y encuentros para esta aventura.
Sinopsis: Una misteriosa llamada de un viejo amigo druida atrae a un reclusivo ranger a la selva del bosque de Valkarr.
Sinopsis: Una misteriosa llamada de un viejo amigo druida atrae a un reclusivo ranger a la selva del bosque de Valkarr.
Después de muchos intentos y errores, finalmente pude hacer un juego completo y publicarlo en YouTube:
Cómo funciona
Landing Page
La página de destino es una imagen de gran héroe (generada en Midjourney con un prompt de “El poder del D20”). he añadido algo simple en la animación usando la nueva regla de estilo CSS @starting:
.fade-in {
@apply opacity-100 transition-opacity duration-1000 ease-in-out;
@starting-style {
opacity: 0;
}
}
Authentication
Para jugar el inicio rápido, necesito una cuenta de usuario. Esto es para evitar ser cargado un montón de dinero debido a personas anónimas o bots que usan mis APIs.El clerohace que sea muy sencillo añadir administración de usuarios, y los uso en todos mis proyectos.
Además, tengo un seguimiento del uso de token donde limitaré el uso con un sistema de token, donde comienzas con suficientes tokens para hacer un juego a través de la demo, luego puedes comprar más a medida que vayas.
The First Turn
Cuando el usuario aterriza en la página de aventura para el inicio rápido, lo primero que sucede es que cargamos los datos para la aventura demo, que es solo un archivo JSON simple (como esteUn módulo de aventura o plan en mi sistema se compone de una serie de encuentros, que están conectados entre sí con instrucciones para el LLM:
"encounters": [
{
"id": "broken-silence",
"title": "Broken Silence",
"intro": "Thalbern, a solitary ranger of the Valkarr woods, has always trusted the silence of the wilds more than the promises of men. Orphaned by border raiders and raised by the elves of the Valkrarr Forest, he has spent years living on the edge of Kordavos, guiding travelers, hunting for his own survival, and keeping his distance from the tangled politics of the city.\n\nYet on this night, a message delivered by a red squirrel bearing the unmistakable script of Wollandora, a trusted elven friend and druid, has drawn him from his hidden home. The note was simple and urgent: Meet me at the Old Standing Stones at midnight. The balance of the forest could depend on it.\n\nNow, as midnight approaches, Thalbern moves quietly through the dense undergrowth, guided by memory and instinct. It is dark with almost no moonlight coming through the forest canopy.\n\nSuddenly, the hush of the night is broken by a sharp crack. Something large has just stepped on a branch somewhere off in the distance.",
"instructions": "A perception check is appropriate if Thalbern investigates (low difficulty with a plus 3 modifier). If successful, he will determine it is a large creature that is approaching quickly. With a high roll (18+), he will determine it is an Owlbear. If combat ensues and Thalbern is below 25% health, Wollandora will intervene. If Thalbern avoids or defeats the Owlbear, or if Wollandora saves him, he proceeds to the Old Standing Stones.",
"image": "images/settings/realm-of-myr/the-midnight-summons/broken-silence-2.png",
"transitions": [
{
"condition": "If Thalbern successfully uses stealth to evade and proceeds cautiously towards the Standing Stones, go to meeting-at-stones.",
"encounter": "meeting-at-stones"
},
{
"condition": "If Thalbern fails a perception check, advance to owlbear-confrontation.",
"encounter": "owlbear-confrontation"
},
{
"condition": "If Thalbern fails any dice roll (including stealth, perception, or any other check), advance to owlbear-confrontation.",
"encounter": "owlbear-confrontation"
},
{
"condition": "If Thalbern does NOT successfully use stealth to evade, go to owlbear-confrontation.",
"encounter": "owlbear-confrontation"
},
{
"condition": "If Thalbern does nothing or takes no action, go to owlbear-confrontation.",
"encounter": "owlbear-confrontation"
},
{
"condition": "If Thalbern has a healthPercent of less than 50%, go to wollandora-intervention.",
"encounter": "wollandora-intervention"
}
]
},
{
"id": "owlbear-confrontation",
"title": "Owlbear Confrontation",
"intro": "From the direction of the sound, a little bit of eye shine glints in the shadows of the tree line. A hulking fifteen foot tall monster with the body of a giant bear and the head of an owl. As it crashes out from the undergrowth, it lets out a guttural squawk, clearly agitated and territorial.",
"instructions": "The Owlbear will attack. If Thalbern attempts an animal handling check (high difficulty) and succeeds, he can move past the Owlbear. If Thalbern wins initiative and attempts to hide, he can move past the Owlbear if he passes a medium difficulty stealth check. If Thalbern's health drops to a critical level, Wollandora appears and drives off the Owlbear, transitioning to 'wollandora-intervention'. If Thalbern defeats the Owlbear, describe his victory and transition to 'meeting-at-stones'.",
"image": "images/settings/realm-of-myr/the-midnight-summons/owlbear-confrontation.png",
"npc": [
{
"id": "owlbear",
"behavior": "Aggressively attacks any perceived threat. Will fight until heavily wounded or driven off.",
"initialInitiative": 1
}
],
"transitions": [
{
"condition": "Thalbern defeats the Owlbear, manages to evade it, successfully uses Animal Handling to pacify and move past it, or successfully rolls any other way to move past it.",
"encounter": "meeting-at-stones"
},
{
"condition": "Thalbern is reduced to critical health by the Owlbear.",
"encounter": "timely-rescue"
}
]
},
...
]
The First Reply
Dado que estamos en modo demo, no hay ninguna aventura real creada todavía en el backend. Esto sucede cuando el jugador hace la primera respuesta.formatNarrativeAction
Función que utiliza la IA para evaluar la respuesta para asegurarse de que sea gramaticalmente correcta, en la tercera persona y añade diálogo u otra prosa para hacer que funcione bien en un estilo narrativo literario.
Después de que se aplique la formatación, la respuesta se envía a una acción del servidor. Debido a que esta es una demostración, estamos creando la aventura en la base de datos cuando se recibe esta primera respuesta.Después de que esto se haga, seguirá el flujo normal para procesar una respuesta del jugador.
Processing Player Responses
ElprocessTurnReply
La función carga el giro actual de Convex, los datos de aventura de S3, y identifica el encuentro específico y el personaje que realiza la acción.
Con este contexto, luego utiliza la IA para determinar si la acción es plausible (mi hijo durante las pruebas de juego tuvo que lanzar el ranger un nuke en el oso) y, en caso afirmativo, si se requiere mecánicamente un rollo de dígitos (por ejemplo, un "Rollo de ataque" o un "Check Stealth"), incluyendo el tipo de rollo y su dificultad.
Podemos hacer esto con una llamada de función, donde podemos especificar a la IA que queremos que los datos estructurados se devuelvan, en este caso unrollRequirementSchema
:
import { z } from "zod";
export const rollRequirementSchema = z.union([
z.object({
rollType: z.string().describe("The type of roll required, e.g. 'Stealth Check'"),
difficulty: z.number().describe("The difficulty class (DC) for the roll"),
modifier: z.number().optional().describe("Bonus or penalty to the roll, e.g. +2 or -1"),
}),
z.null()
]);
export type RollRequirement = z.infer<typeof rollRequirementSchema>;
Entonces tenemos una función que envía un prompt detallado y el esquema agenerateObject
:
export async function getRollRequirementForAction(action: string) {
const prompt = `
Given the following player or NPC action, determine if a dice roll is required for the character to attempt the action. If a roll is required, return a JSON object with "rollType" (choose the most appropriate from the list below) and "difficulty" (a number between 5 and 25). If no roll is required, return the JSON value null (not a string).
Possible roll types:
- Perception Check
- Investigation Check
- Insight Check
- Stealth Check
...
Examples:
Action: "Try to sneak past the guards."
Result: { "rollType": "Stealth Check", "difficulty": 15 }
Action: "Attack the goblin."
Result: { "rollType": "Attack Roll", "difficulty": 12 }
Action: "Try to determine what the sound is."
Result: { "rollType": "Perception Check", "difficulty": 10 }
Action: "Say hello."
Result: null
Now, given the following action, determine the roll requirement.
Action: "${action}"
`;
try {
const result = await generateObject({
schema: rollRequirementSchema,
prompt,
});
if (
result.object &&
typeof result.object === "object" &&
"rollType" in result.object &&
(result.object.rollType === "null" || result.object.rollType === "none" || result.object.rollType === "")
) {
return null;
}
return result.object ?? null;
} catch (error) {
throw error;
}
}
Después de añadir la respuesta del jugador a la historia de la vuelta, si se requiere un rollo de dígitos, actualizamos el estado del personaje en los datos de la vuelta convexa para reflejar que han respondido pero su turno no está completo. Esta actualización provoca una actualización en tiempo real a la interfaz mostrando los detalles del rollo y dándole al jugador un botón para rollar un D20.
Una vez que el usuario roza, tenemos unaresolvePlayerRollResult
Acción del servidor que actualiza la narración con una visualización del resultado y escribe prosa que describe el resultado.También tenemos otra llamada de la función de IA para actualizar la salud y el estado de todos los personajes en el turno.
NPC Actions
Cuando se trata de un NPC para responder, seguimos un patrón similar como un jugador, salvo que en este caso la IA escriba la respuesta. Cada encuentro incluye información sobre la motivación de los NPC y cuando se combina con el contexto de la narración de la aventura hasta ahora, esperemos que la IA pueda generar una buena respuesta, luego generar su propio rollo de dígitos y actualización de resultados.
Training AI to Run RPGs
Si alguna vez has intentado hacer una sesión de juego con AI en un chat, sabes lo rápido que puede salir de la pista.
Esperemos que al proporcionar el contexto adecuado, instrucciones específicas y llamadas de funciones de datos estructuradas, podamos obtener una experiencia que sea agradable.
Por ejemplo, cuando pregunté por qué la IA no siguió las instrucciones de encuentro en una de mis pruebas, esto es lo que el chat de Cursor tenía que decirme:
En resumen, el LLM no siguió sus instrucciones explícitas para la transición cuando un personaje falla una verificación de percepción, a pesar de que se proporcionó evidencia clara de tal fracaso y una regla de transición para ese escenario específico.
En resumen, el LLM no siguió sus instrucciones explícitas para la transición cuando un personaje falla una verificación de percepción, a pesar de que se proporcionó evidencia clara de tal fracaso y una regla de transición para ese escenario específico.
Supongo que la aleatoriedad de la IA no hacer exactamente lo que se espera podría ser parte de la diversión.
Aquí hay un ejemplo deUna de las sesiones de juegos.
Puedes ver el código fuente completo de este proyecto engithub.com/johnpolacek/d20adventures.com