Building an Infinite Craft clone with Tactics
Building an Infinite Craft clone with Tactics
Building an Infinite Craft clone with Tactics
Gabe Alfour
Dec 5, 2024
InfiniteCraft is a fun game built by Neal Agarwal.
You combine existing elements (words, concepts or idioms) to make new ones. It is a bit of a sandbox game, you can just explore and make fun combinations. Some play it with a more specific goal, such as discovering words that no one discovered, or finding a word as fast as possible.
I love this type of little game, they are fun and simple. This fits my philosophy for app development - simple apps that do one thing well. Unfortunately, the dev-ops involved have in the past discouraged me multiple times from building such things.
With Tactics, fortunately, this is now trivial. Tactics manages the entire backend for me (including LLM calls), and I can focus on building the core logic of my app, as well as the frontend.
Tactics is a platform dedicated to building web backends with first-class AI integration. It is in alpha for now, but I was still able to use it to build Endless Alchemy. You can play it there!
Endless Alchemy is an InfiniteCraft clone where the goal is to reach to word House in as few combinations as possible. It features a leaderboard and cheat prevention, with the data store and authentication fully managed by Tactics.
In this post, I will show how I used Tactics to implement the core logic, caching and an anti-cheat system.
1. Our First Tactic
The core of Infinite Craft is a short program used to combine words. For instance, in Infinite Craft, if you combine “Water” and “Flame”, you get “Steam”.
In Tactics, each small program is called… a “Tactic”. And a bundle of such programs is called a “Workspace”. So to write the core logic, we’ll create a Workspace, and then a Tactic for it.
I login, and then go to my dashboard. I create a new Workspace. I name it, and create a new Tactic in it.
I write a basic prompt, and paste it into the IDE.
To test it, I set some initial variables. I set the first to “earth” and the second to “wind”. I click run, and I get… “dust”.
It’s fairly easy to test it with different combinations, but I am mostly happy with this result. My goal is not to replace Infinite Craft with a better engine, it is just to see how natural it is to write it in Tactics!
2. Sessions and Anti-Cheat
A small problem here (that also exists on the OG InfiniteCraft) is that people can cheat!
In InfiniteCraft, we start from four elements (Earth, Wind, Fire and Water), but we can actually call its API with arbitrary words.
Let’s protect ourselves against this with Tactic, by ensuring that a player can only use words that they have discovered in their current playing session.
Tactics has first-class support for sessions, so the code to do so is quite easy to write. The backend will automatically generate a session whenever someone connects to it, and the frontend simply has to maintain a session ID.
The Tactic looks like this:
// Set the initial elements if the session is new if ($session.isNew({})) { $session.setKey({ "key" : "element-fire" , "value" : true }) ; $session.setKey({ "key" : "element-water" , "value" : true }) ; $session.setKey({ "key" : "element-earth" , "value" : true }) ; $session.setKey({ "key" : "element-wind" , "value" : true }) ; } // Check that the queried element (first and second) have been discovered in this session check1 = $session.getKey({ "key" : `element-${first}`}); check2 = $session.getKey({ "key" : `element-${second}`}); if(check1 === null) { return { "failure": `missing element ${first}` }; } if(check2 === null) { return { "failure": `missing element ${first}` }; } // Core Logic combined = $do `You are the engine behind a game. Your job is to combine two words/idioms into a new one The combinations should look for, something that is semantically "in between" the two inputs. For instance "fire" + "water" would yield "steam". Just respond with the output word, no chit-chat, no thoughts, just say the first that comes to your mind and stop. The inputs are "${first}" and "${second}".` ; // Assert that the combination has been discovered during the session $session.setKey({ "key" : `element-${combined.word}`, "value": true }); // Success! return { "success" : combined } ;
If I try it with an element that I have not discovered, I can see that it fails…
Even better, in the IDE, I can just mock and change the session easily in the store tab! Here, I removed “earth” from the accessible elements, and I added “fake”.
3. Database and Cache
An important part of InfiniteCraft is that LLM calls are cached. In other words, the first time someone triggers a new combination (like “steam” and “enigma”), the result of the LLM call is stored. That stored result will be fetched and used the next time someone triggers the same combination.
This solves two problems at once:
LLM Calls are slower and more expensive than database accesses. Caching them is more efficient.
LLM Calls are non-deterministic, and it would be unfair if different users got different results out of using them.
Fortunately, such a simple thing is trivial to implement in Tactics. Each Workspace has an associated database that we can use without having to set up anything.
To do so, we use a syntax close to the one used for the sessions.
Here is the code:
// Set the initial elements if the session is new if ($session.isNew({})) { $session.setKey({ "key" : "element-fire" , "value" : true }) ; $session.setKey({ "key" : "element-water" , "value" : true }) ; $session.setKey({ "key" : "element-earth" , "value" : true }) ; $session.setKey({ "key" : "element-wind" , "value" : true }) ; } // Check that the queried element (first and second) have been discovered in this session check1 = $session.getKey({ "key" : `element-${first}`}); check2 = $session.getKey({ "key" : `element-${second}`}); if(check1 === null) { return { "failure": `missing element ${first}` }; } if(check2 === null) { return { "failure": `missing element ${first}` }; } // The Workspace database is addressed by keys (also know as a Key-Value Store) // We create a key that identifies the combination, and check if it exists in the cache key = `${first}/${second}`; cached = $workspace.getKey({"key": key}); if (cached === null) { // If the result was not cached, we execute the core logic... combined = $do `You are the engine behind a game. Your job is to combine two words/idioms into a new one The combinations should look for, something that is semantically "in between" the two inputs. For instance "fire" + "water" would yield "steam". Just respond with the output word, no chit-chat, no thoughts, just say the first that comes to your mind and stop. The inputs are "${first}" and "${second}".` ; // ...And we do not forget to cache it! cached = $workspace.setKey({"key": key, "value": output}); } else { // If the result was cached, then we just set `combined` to it. There is nothing to do. combined = cached ; } // Assert that the combination has been discovered during the session $session.setKey({ "key" : `element-${combined.word}`, "value": true }); // Success! return { "success" : combined } ;
4. API and Frontend???
Now, you can start using your Tactics as an API!
Check it out at Endless Alchemy for an example of a full stack app that you can build with Tactics. No special magic in there, the same Tactics that anyone can use!
The docs are a bit iffy for now, so once they’re improved, I’ll follow up later for how to use the APIs that are linked to your workspace to build a frontend.
That’s what happens when you are early on the trend curve of a new tool 😀
If you like this hacky spirit and want to become one of our alpha testers, let us know at https://discord.gg/AjZKAURk9f, or sign up directly here: alpha.conjecture.dev! We can fill you in on our roadmap, tell you all about the undocumented features and you can participate in building the future of Tactics!
InfiniteCraft is a fun game built by Neal Agarwal.
You combine existing elements (words, concepts or idioms) to make new ones. It is a bit of a sandbox game, you can just explore and make fun combinations. Some play it with a more specific goal, such as discovering words that no one discovered, or finding a word as fast as possible.
I love this type of little game, they are fun and simple. This fits my philosophy for app development - simple apps that do one thing well. Unfortunately, the dev-ops involved have in the past discouraged me multiple times from building such things.
With Tactics, fortunately, this is now trivial. Tactics manages the entire backend for me (including LLM calls), and I can focus on building the core logic of my app, as well as the frontend.
Tactics is a platform dedicated to building web backends with first-class AI integration. It is in alpha for now, but I was still able to use it to build Endless Alchemy. You can play it there!
Endless Alchemy is an InfiniteCraft clone where the goal is to reach to word House in as few combinations as possible. It features a leaderboard and cheat prevention, with the data store and authentication fully managed by Tactics.
In this post, I will show how I used Tactics to implement the core logic, caching and an anti-cheat system.
1. Our First Tactic
The core of Infinite Craft is a short program used to combine words. For instance, in Infinite Craft, if you combine “Water” and “Flame”, you get “Steam”.
In Tactics, each small program is called… a “Tactic”. And a bundle of such programs is called a “Workspace”. So to write the core logic, we’ll create a Workspace, and then a Tactic for it.
I login, and then go to my dashboard. I create a new Workspace. I name it, and create a new Tactic in it.
I write a basic prompt, and paste it into the IDE.
To test it, I set some initial variables. I set the first to “earth” and the second to “wind”. I click run, and I get… “dust”.
It’s fairly easy to test it with different combinations, but I am mostly happy with this result. My goal is not to replace Infinite Craft with a better engine, it is just to see how natural it is to write it in Tactics!
2. Sessions and Anti-Cheat
A small problem here (that also exists on the OG InfiniteCraft) is that people can cheat!
In InfiniteCraft, we start from four elements (Earth, Wind, Fire and Water), but we can actually call its API with arbitrary words.
Let’s protect ourselves against this with Tactic, by ensuring that a player can only use words that they have discovered in their current playing session.
Tactics has first-class support for sessions, so the code to do so is quite easy to write. The backend will automatically generate a session whenever someone connects to it, and the frontend simply has to maintain a session ID.
The Tactic looks like this:
// Set the initial elements if the session is new if ($session.isNew({})) { $session.setKey({ "key" : "element-fire" , "value" : true }) ; $session.setKey({ "key" : "element-water" , "value" : true }) ; $session.setKey({ "key" : "element-earth" , "value" : true }) ; $session.setKey({ "key" : "element-wind" , "value" : true }) ; } // Check that the queried element (first and second) have been discovered in this session check1 = $session.getKey({ "key" : `element-${first}`}); check2 = $session.getKey({ "key" : `element-${second}`}); if(check1 === null) { return { "failure": `missing element ${first}` }; } if(check2 === null) { return { "failure": `missing element ${first}` }; } // Core Logic combined = $do `You are the engine behind a game. Your job is to combine two words/idioms into a new one The combinations should look for, something that is semantically "in between" the two inputs. For instance "fire" + "water" would yield "steam". Just respond with the output word, no chit-chat, no thoughts, just say the first that comes to your mind and stop. The inputs are "${first}" and "${second}".` ; // Assert that the combination has been discovered during the session $session.setKey({ "key" : `element-${combined.word}`, "value": true }); // Success! return { "success" : combined } ;
If I try it with an element that I have not discovered, I can see that it fails…
Even better, in the IDE, I can just mock and change the session easily in the store tab! Here, I removed “earth” from the accessible elements, and I added “fake”.
3. Database and Cache
An important part of InfiniteCraft is that LLM calls are cached. In other words, the first time someone triggers a new combination (like “steam” and “enigma”), the result of the LLM call is stored. That stored result will be fetched and used the next time someone triggers the same combination.
This solves two problems at once:
LLM Calls are slower and more expensive than database accesses. Caching them is more efficient.
LLM Calls are non-deterministic, and it would be unfair if different users got different results out of using them.
Fortunately, such a simple thing is trivial to implement in Tactics. Each Workspace has an associated database that we can use without having to set up anything.
To do so, we use a syntax close to the one used for the sessions.
Here is the code:
// Set the initial elements if the session is new if ($session.isNew({})) { $session.setKey({ "key" : "element-fire" , "value" : true }) ; $session.setKey({ "key" : "element-water" , "value" : true }) ; $session.setKey({ "key" : "element-earth" , "value" : true }) ; $session.setKey({ "key" : "element-wind" , "value" : true }) ; } // Check that the queried element (first and second) have been discovered in this session check1 = $session.getKey({ "key" : `element-${first}`}); check2 = $session.getKey({ "key" : `element-${second}`}); if(check1 === null) { return { "failure": `missing element ${first}` }; } if(check2 === null) { return { "failure": `missing element ${first}` }; } // The Workspace database is addressed by keys (also know as a Key-Value Store) // We create a key that identifies the combination, and check if it exists in the cache key = `${first}/${second}`; cached = $workspace.getKey({"key": key}); if (cached === null) { // If the result was not cached, we execute the core logic... combined = $do `You are the engine behind a game. Your job is to combine two words/idioms into a new one The combinations should look for, something that is semantically "in between" the two inputs. For instance "fire" + "water" would yield "steam". Just respond with the output word, no chit-chat, no thoughts, just say the first that comes to your mind and stop. The inputs are "${first}" and "${second}".` ; // ...And we do not forget to cache it! cached = $workspace.setKey({"key": key, "value": output}); } else { // If the result was cached, then we just set `combined` to it. There is nothing to do. combined = cached ; } // Assert that the combination has been discovered during the session $session.setKey({ "key" : `element-${combined.word}`, "value": true }); // Success! return { "success" : combined } ;
4. API and Frontend???
Now, you can start using your Tactics as an API!
Check it out at Endless Alchemy for an example of a full stack app that you can build with Tactics. No special magic in there, the same Tactics that anyone can use!
The docs are a bit iffy for now, so once they’re improved, I’ll follow up later for how to use the APIs that are linked to your workspace to build a frontend.
That’s what happens when you are early on the trend curve of a new tool 😀
If you like this hacky spirit and want to become one of our alpha testers, let us know at https://discord.gg/AjZKAURk9f, or sign up directly here: alpha.conjecture.dev! We can fill you in on our roadmap, tell you all about the undocumented features and you can participate in building the future of Tactics!
InfiniteCraft is a fun game built by Neal Agarwal.
You combine existing elements (words, concepts or idioms) to make new ones. It is a bit of a sandbox game, you can just explore and make fun combinations. Some play it with a more specific goal, such as discovering words that no one discovered, or finding a word as fast as possible.
I love this type of little game, they are fun and simple. This fits my philosophy for app development - simple apps that do one thing well. Unfortunately, the dev-ops involved have in the past discouraged me multiple times from building such things.
With Tactics, fortunately, this is now trivial. Tactics manages the entire backend for me (including LLM calls), and I can focus on building the core logic of my app, as well as the frontend.
Tactics is a platform dedicated to building web backends with first-class AI integration. It is in alpha for now, but I was still able to use it to build Endless Alchemy. You can play it there!
Endless Alchemy is an InfiniteCraft clone where the goal is to reach to word House in as few combinations as possible. It features a leaderboard and cheat prevention, with the data store and authentication fully managed by Tactics.
In this post, I will show how I used Tactics to implement the core logic, caching and an anti-cheat system.
1. Our First Tactic
The core of Infinite Craft is a short program used to combine words. For instance, in Infinite Craft, if you combine “Water” and “Flame”, you get “Steam”.
In Tactics, each small program is called… a “Tactic”. And a bundle of such programs is called a “Workspace”. So to write the core logic, we’ll create a Workspace, and then a Tactic for it.
I login, and then go to my dashboard. I create a new Workspace. I name it, and create a new Tactic in it.
I write a basic prompt, and paste it into the IDE.
To test it, I set some initial variables. I set the first to “earth” and the second to “wind”. I click run, and I get… “dust”.
It’s fairly easy to test it with different combinations, but I am mostly happy with this result. My goal is not to replace Infinite Craft with a better engine, it is just to see how natural it is to write it in Tactics!
2. Sessions and Anti-Cheat
A small problem here (that also exists on the OG InfiniteCraft) is that people can cheat!
In InfiniteCraft, we start from four elements (Earth, Wind, Fire and Water), but we can actually call its API with arbitrary words.
Let’s protect ourselves against this with Tactic, by ensuring that a player can only use words that they have discovered in their current playing session.
Tactics has first-class support for sessions, so the code to do so is quite easy to write. The backend will automatically generate a session whenever someone connects to it, and the frontend simply has to maintain a session ID.
The Tactic looks like this:
// Set the initial elements if the session is new if ($session.isNew({})) { $session.setKey({ "key" : "element-fire" , "value" : true }) ; $session.setKey({ "key" : "element-water" , "value" : true }) ; $session.setKey({ "key" : "element-earth" , "value" : true }) ; $session.setKey({ "key" : "element-wind" , "value" : true }) ; } // Check that the queried element (first and second) have been discovered in this session check1 = $session.getKey({ "key" : `element-${first}`}); check2 = $session.getKey({ "key" : `element-${second}`}); if(check1 === null) { return { "failure": `missing element ${first}` }; } if(check2 === null) { return { "failure": `missing element ${first}` }; } // Core Logic combined = $do `You are the engine behind a game. Your job is to combine two words/idioms into a new one The combinations should look for, something that is semantically "in between" the two inputs. For instance "fire" + "water" would yield "steam". Just respond with the output word, no chit-chat, no thoughts, just say the first that comes to your mind and stop. The inputs are "${first}" and "${second}".` ; // Assert that the combination has been discovered during the session $session.setKey({ "key" : `element-${combined.word}`, "value": true }); // Success! return { "success" : combined } ;
If I try it with an element that I have not discovered, I can see that it fails…
Even better, in the IDE, I can just mock and change the session easily in the store tab! Here, I removed “earth” from the accessible elements, and I added “fake”.
3. Database and Cache
An important part of InfiniteCraft is that LLM calls are cached. In other words, the first time someone triggers a new combination (like “steam” and “enigma”), the result of the LLM call is stored. That stored result will be fetched and used the next time someone triggers the same combination.
This solves two problems at once:
LLM Calls are slower and more expensive than database accesses. Caching them is more efficient.
LLM Calls are non-deterministic, and it would be unfair if different users got different results out of using them.
Fortunately, such a simple thing is trivial to implement in Tactics. Each Workspace has an associated database that we can use without having to set up anything.
To do so, we use a syntax close to the one used for the sessions.
Here is the code:
// Set the initial elements if the session is new if ($session.isNew({})) { $session.setKey({ "key" : "element-fire" , "value" : true }) ; $session.setKey({ "key" : "element-water" , "value" : true }) ; $session.setKey({ "key" : "element-earth" , "value" : true }) ; $session.setKey({ "key" : "element-wind" , "value" : true }) ; } // Check that the queried element (first and second) have been discovered in this session check1 = $session.getKey({ "key" : `element-${first}`}); check2 = $session.getKey({ "key" : `element-${second}`}); if(check1 === null) { return { "failure": `missing element ${first}` }; } if(check2 === null) { return { "failure": `missing element ${first}` }; } // The Workspace database is addressed by keys (also know as a Key-Value Store) // We create a key that identifies the combination, and check if it exists in the cache key = `${first}/${second}`; cached = $workspace.getKey({"key": key}); if (cached === null) { // If the result was not cached, we execute the core logic... combined = $do `You are the engine behind a game. Your job is to combine two words/idioms into a new one The combinations should look for, something that is semantically "in between" the two inputs. For instance "fire" + "water" would yield "steam". Just respond with the output word, no chit-chat, no thoughts, just say the first that comes to your mind and stop. The inputs are "${first}" and "${second}".` ; // ...And we do not forget to cache it! cached = $workspace.setKey({"key": key, "value": output}); } else { // If the result was cached, then we just set `combined` to it. There is nothing to do. combined = cached ; } // Assert that the combination has been discovered during the session $session.setKey({ "key" : `element-${combined.word}`, "value": true }); // Success! return { "success" : combined } ;
4. API and Frontend???
Now, you can start using your Tactics as an API!
Check it out at Endless Alchemy for an example of a full stack app that you can build with Tactics. No special magic in there, the same Tactics that anyone can use!
The docs are a bit iffy for now, so once they’re improved, I’ll follow up later for how to use the APIs that are linked to your workspace to build a frontend.
That’s what happens when you are early on the trend curve of a new tool 😀
If you like this hacky spirit and want to become one of our alpha testers, let us know at https://discord.gg/AjZKAURk9f, or sign up directly here: alpha.conjecture.dev! We can fill you in on our roadmap, tell you all about the undocumented features and you can participate in building the future of Tactics!
Latest Articles
Dec 13, 2024
Build a Simple Game with Tactics!
Build a Simple Game with Tactics!
Learn how to write a simple text-based AI game with Tactics, and check out games for inspiration at tactics.dev/games
Dec 5, 2024
Building an Infinite Craft clone with Tactics
Building an Infinite Craft clone with Tactics
Using Tactics to build a simple web app
Dec 2, 2024
Conjecture: A Roadmap for Cognitive Software and A Humanist Future of AI
Conjecture: A Roadmap for Cognitive Software and A Humanist Future of AI
An overview of Conjecture's approach to "Cognitive Software," and our build path towards a good future.
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