Title
Title

Build a Simple Game with Tactics!

Build a Simple Game with Tactics!

Build a Simple Game with Tactics!

Mason Edwards

Dec 13, 2024

This is a tutorial to build a simple LLM Game with Tactics, written for a semi-technical audience. While there's some code involved, it is pretty simple to follow. Check out some pre-built games at tactics.dev/games!

Tactics as a tool is for developers of all skill levels. It lets you take ideas and turn them into apps quickly, and solves the headaches that would come from trying to program a backend yourself.

Today, we’ll make a game where you have 3 lives and try to guess as many animals as you can! It’ll look something like this:

The games we can build using Tactics all follow a similar format: they are text-based games played through a chat window, that use LLMs to take care of the game flow, and share a common UIX which we host. Think CharacterAI, but for games that allow players to use their creativity to win! When you make a game, you control and can customise all the content of the game, how user-AI interactions happen, and what’s tracked in the “game state.

Let's dive in!

(at the bottom of this post there is a visual representation of the how the UI and Tactics interact to make the game engine)

Step 1 : Accounts and Your First Tactic 

Start at https://tactics.dev and create an account. You’ll be redirected to Workspaces, which are groups of tactics. You’ll need to log in to create your own workspace and tactics – feel free to rename the default workspace there!

Then create a tactic, call it “my first game” (or whatever you like) and click on the settings tab. By default your game will use the AI model LLama3. Set the temperature to 1.0, which controls how random the output will be. This is really high, but lets us get more random animals to guess easily. 

Step 2: Game Setup 

Head on over to tactics.dev/games and click on "Create Game." Once you load the game you’ll be able to fill in the name, description, and add an image and tags. (Hint: games look better with banner size images!) 

You'll also be asked to link a tactic. Here, you'll need to paste in the URL of your tactic, which you can get from your workspace dashboard. Navigate to the dashboard, then click on "Manage Tactic." You can find the full URL in the first line of the API call, here, what starts with "https://api.tactics.dev/api…"

Then, create the game. Congratulations! You now have an entirely hosted LLM game. It does absolutely nothing right now… but we can fix that. Your game is already live, right away, so you can keep this tab open and go back to your tactic.

Step 3: Tactic Permissions (optional)

Tactics should run in your game fine by default, but if anything seems awry or your code is not running, check the permissions of your Tactic. In your workspace, hover over the tactic you, and go to "Manage tactic". On the Settings tab, enable “any tactic” to call this tactic and “anyone” to execute this tactic.

Now just make sure that the tactic and anyone in the settings tab at the top. Then, go back to your tactic! 

If you'd like others to be able to let other people see the code of your tactic, you can change your workspace to permissions to "public." You can also host your game on another website through an API!

Step 4: Building the Game 

We’ll start working on the game now. 

Here's a link to the full tactic we're going to write.

Below, we'll walk through the meaningful parts of the code in order.

The first thing I do is write a function.

animal_and_fact = fun () => { 
  animal = $do `think of an animal and respond with that animal`;
  animal_facts = $do `give me a fact about this animal ${animal} just the fact, nothing else and dont mention the animal name at all!`;
  return [animal, animal_facts];
};

(The formatting here just makes the code a little DRY-ER. So if you don't want to use the function it's personal preference.) 

The function is called animal_and_fact. When we call this function, we’re asking an LLM to give us an animal and then a fact about this animal in an array. That's what the $do `prompt` call does - it passes the text here into an LLM and spits out the response. For more technical readers, check out the docs to see how to do this even faster by using a schema to control the output of the LLM.

Now we need to update the game stage engine so we can actually play the game.

Every time we run this tactic, we’re passing a dictionary called state to the frontend. This gets updated and passed back to the tactic, which runs some code, and then passes state back to the frontend again. That’s how the handshake works. This function is extremely versatile, but it expects a few things to make it work:

  • State.status = “String”. This is the state of the game engine. The first time the game is loaded it’ll be called “INIT”. The game finishes when you set it to “FINISH”. When you want to stop executing and wait for the user to type something you set it to “INPUT_NEXT_ACTION”. You can have any intermediary steps you like. 

  • state.observables = {key: “value”}Anything you want the user to see in the game state, like lives or score or whatever, goes here. They will see the key and the value. Get creative! 

  • State.player_action = “String”. You don’t really change this, but it's what the UI sends the tactic back as the player's message from the chat. So you read it often. 

  • State.output = “String”. The game’s response to the user in the chat window of the UI 

This’ll make more sense as we write it up, so let's move on. Remember the game is called "INIT" so we can use that to set up our game! 

if (state.status === "INIT") {
  animalArry = animal_and_fact();
  return {
    ...state,
    status: "INPUT_NEXT_ACTION",
    animal: animalArry[0],
    observables: {
      "lives": 3,
      "score": 0
    },
    output: `What animal am I thinking of, here's a fact ${animalArry[1]}`
  };
}

This will now run. You can refresh the game and try. You’ll see lives and score and get an animal fact. But, it can't accept input or do anything. 

Time to fix this.

A game like the one we are writing here is roughly a large state machine, rotating between actions that need to happen. For example, in our game, we'll generate an animal (that's one state), make a user guess the animal (that's a different state), evaluate if the user guessed the animal correctly (that's a third state), and check if the user has enough lives to keep guessing (that's a fourth), and so on.

My tactic will return state as a dictionary passed back and forth between the frontend and the tactics backend. The frontend will pass state back with the field state.status = “INPUT_NEXT_ACTION, so when the user inputs a message in the UI the tactic will understand that state has updated. 

The UIX always requires the states INPUT_NEXT_ACTION, INIT and FINISH. But USER_GUESSED is an example of a state.status that’s specific just to this game. Also notice that in the UI the user doesn’t see “animal” or “status”, since these are not observable in the state dictionary.

Now, let's write the code for what happens when a user guesses an animal.

Below is the code. This lets us update the state, and pass it back with a return. When we evaluate verdict, we prompt the model to evaluate if the guess is fair, and change our score depending on if the guess was correct or not (again, see docs for schema which is a strictly better way to do this). Here, I ask the LLM to just tell me if it’s a match. No regexes, no funny JS magic. Just good-new-fashion in-line LLM program. Tactics are awesome!

If the score goes to 0, the next state is “FINISH” because the player is out of lives. If the player still have lives, then we pass to the next turn and share a new state: “NEW_ANIMAL”

if (state.status === "USER_GUESSED") {
  guess = state.player_action;
  verdict = $do `is ${guess} the same as ${state.animal} or very similar. Be fair. Only respond "yes", "no" and nothing else!`;
  current_lives = state.observables.lives;
  current_score = state.observables.score;
  if (verdict === "yes") {
    current_score = current_score + 1;
    output = "well done!";
  } else {
    current_lives = current_lives - 1
    output = "Sorry, not quite. Let's try again.";
  }
  if (current_lives === 0) {
    status = "FINISH";
    output = "whoops that's all your lives, better luck next time!";
  } else {
    status = "NEW_ANIMAL";
  }
  return { ...state,
    observables: {
      lives: current_lives,
      score: current_score
    },
    status: status,
    output: output
  };
}

This is then passed back to the frontend, which sees if the player has gained score or lost a life.

The next state we need to write is “NEW_ANIMAL”

if (state.status === "NEW_ANIMAL") {
  animalArry = animal_and_fact();
  animal = animalArry[0];
  animalFact = animalArry[1];
  output = `What animal am I thinking of, here's a fact ${animalFact}`;
  state = {
    ...state,
    animal: animal,
    output: output,
    status: "INPUT_NEXT_ACTION"
  };
  return state;
}

Once again, we just return the state after we called that function from before and update the status to “wait for input”. We do tis by setting state.status = “INPUT_NEXT_ACTION”. The UIX from our previous return knows to wait, and when we get the message back, we just bump that along with the state to “USER_GUESSED”. The UI will keep running (passing back state every time state is returned) the tactic after the first time the user inputs a message while state.status != “INPUT_NEXT_ACTION”

if (state.status === "INPUT_NEXT_ACTION") {
  return {
    ...state,
    status: "USER_GUESSED",
    output: ""
  };
}

That’s it! Almost. The last line at the end of the file returns the state, if we missed a status which makes debugging easier.  

// make sure we pass the state back to the UIX!
return state;


Here's a diagram of how it all works


Now, your turn!

You can edit this code as you'd like and customise your game as much as you'd like. Here are some ideas to consider edits to this game:

  • LLM random generation isn’t great. As you will soon learn, LLama is absolutely obsessed with Kangaroos. Instead of generating single animals, you could generate batches of animals and use https://tactics.dev/docs#randomness to pick from the array. You should either remove animals from the array that have been used, or store a list of used animals and inject them into the generation prompt, asking the LLM to not use them.

  • We don't show the Animal that was used for the fact even if you're wrong, which could be updated.

  • We could have the LLM share hints with (this is a bit more involved). 

  • We could even offer a score of how close you were. We could ask the LLM to judge "Is a cat like a lion closer than a dog is to a donkey? Output a score of 1-10!" LLM’s are great at arbitrary comparison. 

Have fun! And feel free to check out more games at https://tactics.dev/games, and the code for some more extensive games here:

This is a tutorial to build a simple LLM Game with Tactics, written for a semi-technical audience. While there's some code involved, it is pretty simple to follow. Check out some pre-built games at tactics.dev/games!

Tactics as a tool is for developers of all skill levels. It lets you take ideas and turn them into apps quickly, and solves the headaches that would come from trying to program a backend yourself.

Today, we’ll make a game where you have 3 lives and try to guess as many animals as you can! It’ll look something like this:

The games we can build using Tactics all follow a similar format: they are text-based games played through a chat window, that use LLMs to take care of the game flow, and share a common UIX which we host. Think CharacterAI, but for games that allow players to use their creativity to win! When you make a game, you control and can customise all the content of the game, how user-AI interactions happen, and what’s tracked in the “game state.

Let's dive in!

(at the bottom of this post there is a visual representation of the how the UI and Tactics interact to make the game engine)

Step 1 : Accounts and Your First Tactic 

Start at https://tactics.dev and create an account. You’ll be redirected to Workspaces, which are groups of tactics. You’ll need to log in to create your own workspace and tactics – feel free to rename the default workspace there!

Then create a tactic, call it “my first game” (or whatever you like) and click on the settings tab. By default your game will use the AI model LLama3. Set the temperature to 1.0, which controls how random the output will be. This is really high, but lets us get more random animals to guess easily. 

Step 2: Game Setup 

Head on over to tactics.dev/games and click on "Create Game." Once you load the game you’ll be able to fill in the name, description, and add an image and tags. (Hint: games look better with banner size images!) 

You'll also be asked to link a tactic. Here, you'll need to paste in the URL of your tactic, which you can get from your workspace dashboard. Navigate to the dashboard, then click on "Manage Tactic." You can find the full URL in the first line of the API call, here, what starts with "https://api.tactics.dev/api…"

Then, create the game. Congratulations! You now have an entirely hosted LLM game. It does absolutely nothing right now… but we can fix that. Your game is already live, right away, so you can keep this tab open and go back to your tactic.

Step 3: Tactic Permissions (optional)

Tactics should run in your game fine by default, but if anything seems awry or your code is not running, check the permissions of your Tactic. In your workspace, hover over the tactic you, and go to "Manage tactic". On the Settings tab, enable “any tactic” to call this tactic and “anyone” to execute this tactic.

Now just make sure that the tactic and anyone in the settings tab at the top. Then, go back to your tactic! 

If you'd like others to be able to let other people see the code of your tactic, you can change your workspace to permissions to "public." You can also host your game on another website through an API!

Step 4: Building the Game 

We’ll start working on the game now. 

Here's a link to the full tactic we're going to write.

Below, we'll walk through the meaningful parts of the code in order.

The first thing I do is write a function.

animal_and_fact = fun () => { 
  animal = $do `think of an animal and respond with that animal`;
  animal_facts = $do `give me a fact about this animal ${animal} just the fact, nothing else and dont mention the animal name at all!`;
  return [animal, animal_facts];
};

(The formatting here just makes the code a little DRY-ER. So if you don't want to use the function it's personal preference.) 

The function is called animal_and_fact. When we call this function, we’re asking an LLM to give us an animal and then a fact about this animal in an array. That's what the $do `prompt` call does - it passes the text here into an LLM and spits out the response. For more technical readers, check out the docs to see how to do this even faster by using a schema to control the output of the LLM.

Now we need to update the game stage engine so we can actually play the game.

Every time we run this tactic, we’re passing a dictionary called state to the frontend. This gets updated and passed back to the tactic, which runs some code, and then passes state back to the frontend again. That’s how the handshake works. This function is extremely versatile, but it expects a few things to make it work:

  • State.status = “String”. This is the state of the game engine. The first time the game is loaded it’ll be called “INIT”. The game finishes when you set it to “FINISH”. When you want to stop executing and wait for the user to type something you set it to “INPUT_NEXT_ACTION”. You can have any intermediary steps you like. 

  • state.observables = {key: “value”}Anything you want the user to see in the game state, like lives or score or whatever, goes here. They will see the key and the value. Get creative! 

  • State.player_action = “String”. You don’t really change this, but it's what the UI sends the tactic back as the player's message from the chat. So you read it often. 

  • State.output = “String”. The game’s response to the user in the chat window of the UI 

This’ll make more sense as we write it up, so let's move on. Remember the game is called "INIT" so we can use that to set up our game! 

if (state.status === "INIT") {
  animalArry = animal_and_fact();
  return {
    ...state,
    status: "INPUT_NEXT_ACTION",
    animal: animalArry[0],
    observables: {
      "lives": 3,
      "score": 0
    },
    output: `What animal am I thinking of, here's a fact ${animalArry[1]}`
  };
}

This will now run. You can refresh the game and try. You’ll see lives and score and get an animal fact. But, it can't accept input or do anything. 

Time to fix this.

A game like the one we are writing here is roughly a large state machine, rotating between actions that need to happen. For example, in our game, we'll generate an animal (that's one state), make a user guess the animal (that's a different state), evaluate if the user guessed the animal correctly (that's a third state), and check if the user has enough lives to keep guessing (that's a fourth), and so on.

My tactic will return state as a dictionary passed back and forth between the frontend and the tactics backend. The frontend will pass state back with the field state.status = “INPUT_NEXT_ACTION, so when the user inputs a message in the UI the tactic will understand that state has updated. 

The UIX always requires the states INPUT_NEXT_ACTION, INIT and FINISH. But USER_GUESSED is an example of a state.status that’s specific just to this game. Also notice that in the UI the user doesn’t see “animal” or “status”, since these are not observable in the state dictionary.

Now, let's write the code for what happens when a user guesses an animal.

Below is the code. This lets us update the state, and pass it back with a return. When we evaluate verdict, we prompt the model to evaluate if the guess is fair, and change our score depending on if the guess was correct or not (again, see docs for schema which is a strictly better way to do this). Here, I ask the LLM to just tell me if it’s a match. No regexes, no funny JS magic. Just good-new-fashion in-line LLM program. Tactics are awesome!

If the score goes to 0, the next state is “FINISH” because the player is out of lives. If the player still have lives, then we pass to the next turn and share a new state: “NEW_ANIMAL”

if (state.status === "USER_GUESSED") {
  guess = state.player_action;
  verdict = $do `is ${guess} the same as ${state.animal} or very similar. Be fair. Only respond "yes", "no" and nothing else!`;
  current_lives = state.observables.lives;
  current_score = state.observables.score;
  if (verdict === "yes") {
    current_score = current_score + 1;
    output = "well done!";
  } else {
    current_lives = current_lives - 1
    output = "Sorry, not quite. Let's try again.";
  }
  if (current_lives === 0) {
    status = "FINISH";
    output = "whoops that's all your lives, better luck next time!";
  } else {
    status = "NEW_ANIMAL";
  }
  return { ...state,
    observables: {
      lives: current_lives,
      score: current_score
    },
    status: status,
    output: output
  };
}

This is then passed back to the frontend, which sees if the player has gained score or lost a life.

The next state we need to write is “NEW_ANIMAL”

if (state.status === "NEW_ANIMAL") {
  animalArry = animal_and_fact();
  animal = animalArry[0];
  animalFact = animalArry[1];
  output = `What animal am I thinking of, here's a fact ${animalFact}`;
  state = {
    ...state,
    animal: animal,
    output: output,
    status: "INPUT_NEXT_ACTION"
  };
  return state;
}

Once again, we just return the state after we called that function from before and update the status to “wait for input”. We do tis by setting state.status = “INPUT_NEXT_ACTION”. The UIX from our previous return knows to wait, and when we get the message back, we just bump that along with the state to “USER_GUESSED”. The UI will keep running (passing back state every time state is returned) the tactic after the first time the user inputs a message while state.status != “INPUT_NEXT_ACTION”

if (state.status === "INPUT_NEXT_ACTION") {
  return {
    ...state,
    status: "USER_GUESSED",
    output: ""
  };
}

That’s it! Almost. The last line at the end of the file returns the state, if we missed a status which makes debugging easier.  

// make sure we pass the state back to the UIX!
return state;


Here's a diagram of how it all works


Now, your turn!

You can edit this code as you'd like and customise your game as much as you'd like. Here are some ideas to consider edits to this game:

  • LLM random generation isn’t great. As you will soon learn, LLama is absolutely obsessed with Kangaroos. Instead of generating single animals, you could generate batches of animals and use https://tactics.dev/docs#randomness to pick from the array. You should either remove animals from the array that have been used, or store a list of used animals and inject them into the generation prompt, asking the LLM to not use them.

  • We don't show the Animal that was used for the fact even if you're wrong, which could be updated.

  • We could have the LLM share hints with (this is a bit more involved). 

  • We could even offer a score of how close you were. We could ask the LLM to judge "Is a cat like a lion closer than a dog is to a donkey? Output a score of 1-10!" LLM’s are great at arbitrary comparison. 

Have fun! And feel free to check out more games at https://tactics.dev/games, and the code for some more extensive games here:

This is a tutorial to build a simple LLM Game with Tactics, written for a semi-technical audience. While there's some code involved, it is pretty simple to follow. Check out some pre-built games at tactics.dev/games!

Tactics as a tool is for developers of all skill levels. It lets you take ideas and turn them into apps quickly, and solves the headaches that would come from trying to program a backend yourself.

Today, we’ll make a game where you have 3 lives and try to guess as many animals as you can! It’ll look something like this:

The games we can build using Tactics all follow a similar format: they are text-based games played through a chat window, that use LLMs to take care of the game flow, and share a common UIX which we host. Think CharacterAI, but for games that allow players to use their creativity to win! When you make a game, you control and can customise all the content of the game, how user-AI interactions happen, and what’s tracked in the “game state.

Let's dive in!

(at the bottom of this post there is a visual representation of the how the UI and Tactics interact to make the game engine)

Step 1 : Accounts and Your First Tactic 

Start at https://tactics.dev and create an account. You’ll be redirected to Workspaces, which are groups of tactics. You’ll need to log in to create your own workspace and tactics – feel free to rename the default workspace there!

Then create a tactic, call it “my first game” (or whatever you like) and click on the settings tab. By default your game will use the AI model LLama3. Set the temperature to 1.0, which controls how random the output will be. This is really high, but lets us get more random animals to guess easily. 

Step 2: Game Setup 

Head on over to tactics.dev/games and click on "Create Game." Once you load the game you’ll be able to fill in the name, description, and add an image and tags. (Hint: games look better with banner size images!) 

You'll also be asked to link a tactic. Here, you'll need to paste in the URL of your tactic, which you can get from your workspace dashboard. Navigate to the dashboard, then click on "Manage Tactic." You can find the full URL in the first line of the API call, here, what starts with "https://api.tactics.dev/api…"

Then, create the game. Congratulations! You now have an entirely hosted LLM game. It does absolutely nothing right now… but we can fix that. Your game is already live, right away, so you can keep this tab open and go back to your tactic.

Step 3: Tactic Permissions (optional)

Tactics should run in your game fine by default, but if anything seems awry or your code is not running, check the permissions of your Tactic. In your workspace, hover over the tactic you, and go to "Manage tactic". On the Settings tab, enable “any tactic” to call this tactic and “anyone” to execute this tactic.

Now just make sure that the tactic and anyone in the settings tab at the top. Then, go back to your tactic! 

If you'd like others to be able to let other people see the code of your tactic, you can change your workspace to permissions to "public." You can also host your game on another website through an API!

Step 4: Building the Game 

We’ll start working on the game now. 

Here's a link to the full tactic we're going to write.

Below, we'll walk through the meaningful parts of the code in order.

The first thing I do is write a function.

animal_and_fact = fun () => { 
  animal = $do `think of an animal and respond with that animal`;
  animal_facts = $do `give me a fact about this animal ${animal} just the fact, nothing else and dont mention the animal name at all!`;
  return [animal, animal_facts];
};

(The formatting here just makes the code a little DRY-ER. So if you don't want to use the function it's personal preference.) 

The function is called animal_and_fact. When we call this function, we’re asking an LLM to give us an animal and then a fact about this animal in an array. That's what the $do `prompt` call does - it passes the text here into an LLM and spits out the response. For more technical readers, check out the docs to see how to do this even faster by using a schema to control the output of the LLM.

Now we need to update the game stage engine so we can actually play the game.

Every time we run this tactic, we’re passing a dictionary called state to the frontend. This gets updated and passed back to the tactic, which runs some code, and then passes state back to the frontend again. That’s how the handshake works. This function is extremely versatile, but it expects a few things to make it work:

  • State.status = “String”. This is the state of the game engine. The first time the game is loaded it’ll be called “INIT”. The game finishes when you set it to “FINISH”. When you want to stop executing and wait for the user to type something you set it to “INPUT_NEXT_ACTION”. You can have any intermediary steps you like. 

  • state.observables = {key: “value”}Anything you want the user to see in the game state, like lives or score or whatever, goes here. They will see the key and the value. Get creative! 

  • State.player_action = “String”. You don’t really change this, but it's what the UI sends the tactic back as the player's message from the chat. So you read it often. 

  • State.output = “String”. The game’s response to the user in the chat window of the UI 

This’ll make more sense as we write it up, so let's move on. Remember the game is called "INIT" so we can use that to set up our game! 

if (state.status === "INIT") {
  animalArry = animal_and_fact();
  return {
    ...state,
    status: "INPUT_NEXT_ACTION",
    animal: animalArry[0],
    observables: {
      "lives": 3,
      "score": 0
    },
    output: `What animal am I thinking of, here's a fact ${animalArry[1]}`
  };
}

This will now run. You can refresh the game and try. You’ll see lives and score and get an animal fact. But, it can't accept input or do anything. 

Time to fix this.

A game like the one we are writing here is roughly a large state machine, rotating between actions that need to happen. For example, in our game, we'll generate an animal (that's one state), make a user guess the animal (that's a different state), evaluate if the user guessed the animal correctly (that's a third state), and check if the user has enough lives to keep guessing (that's a fourth), and so on.

My tactic will return state as a dictionary passed back and forth between the frontend and the tactics backend. The frontend will pass state back with the field state.status = “INPUT_NEXT_ACTION, so when the user inputs a message in the UI the tactic will understand that state has updated. 

The UIX always requires the states INPUT_NEXT_ACTION, INIT and FINISH. But USER_GUESSED is an example of a state.status that’s specific just to this game. Also notice that in the UI the user doesn’t see “animal” or “status”, since these are not observable in the state dictionary.

Now, let's write the code for what happens when a user guesses an animal.

Below is the code. This lets us update the state, and pass it back with a return. When we evaluate verdict, we prompt the model to evaluate if the guess is fair, and change our score depending on if the guess was correct or not (again, see docs for schema which is a strictly better way to do this). Here, I ask the LLM to just tell me if it’s a match. No regexes, no funny JS magic. Just good-new-fashion in-line LLM program. Tactics are awesome!

If the score goes to 0, the next state is “FINISH” because the player is out of lives. If the player still have lives, then we pass to the next turn and share a new state: “NEW_ANIMAL”

if (state.status === "USER_GUESSED") {
  guess = state.player_action;
  verdict = $do `is ${guess} the same as ${state.animal} or very similar. Be fair. Only respond "yes", "no" and nothing else!`;
  current_lives = state.observables.lives;
  current_score = state.observables.score;
  if (verdict === "yes") {
    current_score = current_score + 1;
    output = "well done!";
  } else {
    current_lives = current_lives - 1
    output = "Sorry, not quite. Let's try again.";
  }
  if (current_lives === 0) {
    status = "FINISH";
    output = "whoops that's all your lives, better luck next time!";
  } else {
    status = "NEW_ANIMAL";
  }
  return { ...state,
    observables: {
      lives: current_lives,
      score: current_score
    },
    status: status,
    output: output
  };
}

This is then passed back to the frontend, which sees if the player has gained score or lost a life.

The next state we need to write is “NEW_ANIMAL”

if (state.status === "NEW_ANIMAL") {
  animalArry = animal_and_fact();
  animal = animalArry[0];
  animalFact = animalArry[1];
  output = `What animal am I thinking of, here's a fact ${animalFact}`;
  state = {
    ...state,
    animal: animal,
    output: output,
    status: "INPUT_NEXT_ACTION"
  };
  return state;
}

Once again, we just return the state after we called that function from before and update the status to “wait for input”. We do tis by setting state.status = “INPUT_NEXT_ACTION”. The UIX from our previous return knows to wait, and when we get the message back, we just bump that along with the state to “USER_GUESSED”. The UI will keep running (passing back state every time state is returned) the tactic after the first time the user inputs a message while state.status != “INPUT_NEXT_ACTION”

if (state.status === "INPUT_NEXT_ACTION") {
  return {
    ...state,
    status: "USER_GUESSED",
    output: ""
  };
}

That’s it! Almost. The last line at the end of the file returns the state, if we missed a status which makes debugging easier.  

// make sure we pass the state back to the UIX!
return state;


Here's a diagram of how it all works


Now, your turn!

You can edit this code as you'd like and customise your game as much as you'd like. Here are some ideas to consider edits to this game:

  • LLM random generation isn’t great. As you will soon learn, LLama is absolutely obsessed with Kangaroos. Instead of generating single animals, you could generate batches of animals and use https://tactics.dev/docs#randomness to pick from the array. You should either remove animals from the array that have been used, or store a list of used animals and inject them into the generation prompt, asking the LLM to not use them.

  • We don't show the Animal that was used for the fact even if you're wrong, which could be updated.

  • We could have the LLM share hints with (this is a bit more involved). 

  • We could even offer a score of how close you were. We could ask the LLM to judge "Is a cat like a lion closer than a dog is to a donkey? Output a score of 1-10!" LLM’s are great at arbitrary comparison. 

Have fun! And feel free to check out more games at https://tactics.dev/games, and the code for some more extensive games here:

Sign up to receive our newsletter and
updates on products and services.

Sign up to receive our newsletter and updates on products and services.

Sign up to receive our newsletter and updates on products and services.

Sign Up