A Tetris programmed by AI

To mark the second anniversary of Chat GPT’s release to the public, Bob wanted to offer something more ambitious than last year. I thought that proposing a game entirely developed by AI would be a nice little challenge. Let’s be clear: this isn’t about using AI as a tool to speed up or facilitate development, as I’ve been doing for some time now, but rather about succeeding in creating a “complete” game without touching the code written by the artificial intelligence. A challenge that’s not so simple in practice.

While AI tools can significantly speed up programming, they are not intelligent; they merely develop and format the requests submitted to them. Personally, when working with Unity, I use AI Toolbox, one of the first tools available, which is reliable and regularly updated. The concept is simple: I ask the tool to create (or modify) a C# script in plain language, and I get a ready-to-use script in return. (Once you understand how to communicate effectively with the language model, the results are excellent.) Let me clarify: AI Toolbox doesn’t allow developers to achieve something they don’t already know how to do—because if they don’t know, they won’t know how to ask properly—but it does provide a script almost instantly, saving minutes of writing and/or researching the documentation. Over the course of a full project, that can add up to hours saved!

Examples? Let’s imagine I need a simple command system. I would ask the AI tool something like:

Script Name: Commandes
Close the application.
In the Inspector:
Private string nomEcranTitre
Private GameObject fenêtreAide
Behavior:
If the F1 key is pressed:
Show fenêtreAide if it’s hidden, and hide it if it’s visible.
If the F2 key is pressed:
Load the scene named nomEcranTitre.
If the ESC key is pressed:
Close the application.

I then get a complete script that accounts for exceptions and works perfectly :

Not bad, right? The script itself is very simple, but it’s much faster to write the first window than the second, especially since Chat GPT (the language model used by AI Toolbox) is very tolerant of typos, which is not the case with C#! Let’s take another example, to open a PDF from Unity:

Script Name:”OuverturePdf”
Function:public OuvrirPdf(string nomPDF)In OuvrirPdf, open the PDF file named nomPDF located in the PDF folder.

I made a nice French/English mix (“fonction public”)! And, while these seemingly simple commands often require digging through documentation to find the exact syntax, which can easily take five to ten minutes, with artificial intelligence—bam!—instant script:

It’s clear and functional, with the using statements included… in short, it’s a job well done! Artificial Intelligence is certainly a valuable ally for developers. But is it advanced enough to prevent the developer from having to manually tweak the scripts generated by the AI? Bob is going to check that!

Table of Contents

Roadmap

For my experiment, I chose the venerable Tetris as the ideal guinea pig. Why Tetris? Mainly because, among all the classics I’ve recreated to perfect my skills—remember Bob le Frog from last spring!—Tetris is the only one that has resisted me. I can’t explain it, but all my attempts to recreate this classic, which I’ve been passionate about for the past forty years, have ended in failure. There must be something in the logic of the game that I don’t grasp, even though I’m pretty good when I play it… Anyway, the time has come for me to get my revenge!

But to take on the challenge, I need a roadmap. First thing: I’m not trying to recreate the ultimate Tetris, pixel-perfect and faithful to the official rules. In fact, there are differences between the multiple canonical versions, not to mention all the clones. As soon as I’ve got a functional clone, I’ll consider the challenge complete. And let me remind you, on bobdupneu.fr, it’s Bob who sets the rules! The second thing is, since this is the third time I’m trying to write this article, which has been on my mind for over a year and I’ve failed twice already, it seemed necessary to find a guide: Valem’s tutorial on Tetris will be the perfect fit!

Following a guide will allow me to focus on my requests to Chat GPT, while having a model of what I need to get from the AI to stay on the path to success. And maybe the third time will be the charm!

“Manual” preparation

To keep the challenge simple, I will only use Unity’s tools and follow the Tetris Guideline’s recommendations loosely. I create a new “Universal 2D” project in Unity, naming it TetrIA because I’m a funny guy, and immediately switch to a Web project, since I want to integrate the result of my experiment into the article.

Once the basic setup is done, I create a grid of 10 columns by 20 rows, the 7 official tetriminos, and a simple interface, following the instructions from my guide, Valem.

The screenshot below shows the result. Now, it’s time for Chat GPT to get to work!

Experimentation

Let’s go crazy! Let’s ask Chat GPT to create a Tetris game, simply!

The result seems interesting at first glance…

The only small problem is that the AI plays it safe when it comes to the functions, which are just empty shells…

In short, as I explained earlier, Chat GPT doesn’t replace the programmer, who will need to give precise instructions and come up with the overall game logic themselves, as the AI only speeds up the writing of the code. Nice try, but there’s still some work to be done!

Game Manager

I always start development by creating an empty object called “Game Manager,” to which I attach a “GameManager” script that will gather all the necessary elements for proper game management. My first request to Chat GPT is a bit long but contains nothing complicated:

Script Name: GameManager
Public Variables Attached to the Script:
int Score, Lignes, Niveau, Vitesse
Text T_Score, T_Niveau, T_Lignes
bool JeuEnCours
Private Variables Attached to the Script:
GameObject p_GameOver
GameObject p_Start
In Start():
Initialize Vitesse and Niveau to 1
Call ActualiserUI()
Hide p_GameOver
Show p_Start
Set JeuEnCours to false
In Update():
If JeuEnCours is false and p_Start is visible:
If the player presses the Enter key: JeuEnCours = true
If JeuEnCours is false and p_GameOver is visible:
If the player presses the Enter key: call nouvellePartie()
Public Function ActualiserUI():
T_Score = Display Score with at least 6 digits
T_Niveau = Display Niveau with at least 2 digits
T_Lignes = Display Lignes with at least 3 digits
Public Function GameOver():
JeuEnCours = false
Display p_GameOver
Private Function nouvellePartie():
Reload the scene

I then get a perfectly correct script; I just manually modify the nature of the texts to change them to Text Mesh Pro (which is what I actually used in my UI) and serialize the two panels, Start and Game Over, so I can link them in the inspector.

These aren’t errors from Chat GPT but details I only thought of afterward. So, my script is as follows:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;

public class GameManager : MonoBehaviour
{
    [Tooltip("Score du joueur")]
    public int Score;
    [Tooltip("Nombre de lignes complétées")]
    public int Lignes;
    [Tooltip("Niveau actuel")]
    public int Niveau;
    [Tooltip("Vitesse du jeu")]
    public int Vitesse;

    [Tooltip("Texte affichant le score")]
    public TextMeshProUGUI T_Score;
    [Tooltip("Texte affichant le niveau")]
    public TextMeshProUGUI T_Niveau;
    [Tooltip("Texte affichant les lignes")]
    public TextMeshProUGUI T_Lignes;

    [Tooltip("Indique si le jeu est en cours")]
    public bool JeuEnCours;

    [SerializeField] GameObject p_GameOver;
    [SerializeField] GameObject p_Start;

    void Start()
    {
        Vitesse = 1;
        Niveau = 1;
        ActualiserUI();
        p_GameOver.SetActive(false);
        p_Start.SetActive(true);
        JeuEnCours = false;
    }

    void Update()
    {
        if (!JeuEnCours && p_Start.activeSelf)
        {
            if (Input.GetKeyDown(KeyCode.Return))
            {
                JeuEnCours = true;
                p_Start.SetActive(false);
            }
        }

        if (!JeuEnCours && p_GameOver.activeSelf)
        {
            if (Input.GetKeyDown(KeyCode.Return))
            {
                nouvellePartie();
            }
        }
    }

    public void ActualiserUI()
    {
        T_Score.text = Score.ToString("D6");
        T_Niveau.text = Niveau.ToString("D2");
        T_Lignes.text = Lignes.ToString("D3");
    }

    public void GameOver()
    {
        JeuEnCours = false;
        p_GameOver.SetActive(true);
    }

    private void nouvellePartie()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

This code is fully functional, the necessary using directives are included, and the code used is quite elegant. I launch the game and confirm that everything works (the display updates, and the Start panel disappears when the Enter key is pressed).

It’s time to tackle the Tetris-specific logic.

Move the tetrimino

In Tetris, the tetrimino moves automatically downward, with the speed increasing based on the level. The player can move the piece left and right—by exactly one unit at a time—and accelerate its descent. Additionally, the player can rotate the piece 90° clockwise, and the piece must always stay within the grid. Let’s start with the simplest part: vertical and horizontal movements. A simple request produces a short and fully functional script.

Script name: Tetrimino
Find the object with the GameManager script in the scene and assign it to the private variable _GM
If _GM.JeuEnCours is true:
The GameObject continuously moves downward according to _GM.Vitesse
The GameObject moves 1 unit to the right if the right arrow key is pressed, and 1 unit to the left if the left arrow key is pressed
If the arrow key is held down, the movement occurs every 0.2 seconds
While the down arrow key is pressed, the speed is multiplied by 10

Next, we need to lock the piece in the game area. For reference, the grid has 10 columns and 20 rows. So, we “just” need to check that each of the four squares of every tetrimino is inside this grid after every move, and cancel the move if not. For this, I ask Chat GPT to modify the script I just created.

Define the game grid as 10 x 20 in size.
For every movement of the GameObject, check that each of the GameObject’s children is within this grid and cancel the movement if not. The checks must be performed using integer values.

The request is very simple, but the work done by Chat GPT is quite significant. However, it’s important to note: there’s nothing complex in the code created, but—this is where the AI excels as an assistant to the developer—five lines of request generate three times as many lines of code, not to mention the formatting. Any error in one of these lines, starting with a missing semicolon, leads to a compilation error. The time-saving benefit is clear. In this case, Chat GPT creates the game grid and uses it in a new function to check that the tetrimino is within the game area, without forgetting to modify the existing functions to account for the new feature. Simple and effective!

Now we need to rotate the tetrimino. In the Valem video I’m using as a guide, there’s a very handy chart showing the rotation axes for the seven tetriminos.

So, we need to ask Chat GPT to create this rotation point—the position of which will be adjusted in the inspector for each shape—and to rotate the tetrimino by 90° around this point every time the up arrow key is pressed.

The request is simple, and the code generated is even simpler:

Create a public Vector3 variable called PointRotation.
When the player presses the up arrow key, the GameObject rotates 90° around PointRotation.
The rotation should never allow the Tetrimino to move outside the game grid; cancel the rotation if it does.

The code works, but… when the tetrimino rotates, it no longer descends and moves in the direction of its base (so if it’s upside down, it moves upward!). I quickly identified the issue: the tetrimino’s movement was relative to its orientation, while it should be relative to the grid’s orientation. I explained this to Chat GPT, who modified the script accordingly. Now, the tetrimino moves correctly even after a rotation. Next, I need to manage the stacking of pieces and—an area where I usually get stuck—the disappearance of lines…

The movements of the GameObject are expressed relative to the game grid, not relative to the orientation of the GameObject.

Stacking and line management

In Tetris, the player tries to stack the tetriminos without leaving empty spaces. When a line is complete (all ten cells are filled with a tetrimino block), it disappears, rewarding the player with points. A multiplier is applied when multiple lines disappear at the same time, and the deleted lines are immediately filled with pieces from the lines above. The part where I’ve been stuck is precisely this last one: in my previous attempts, I never managed to achieve a satisfactory line deletion. But today is the day of my triumph: I will succeed!

But in order for the tetriminos to stack, there need to be several of them. And for gameplay reasons, I will make sure that the upcoming tetrimino is announced on the side. The scene will display two tetriminos, one of which is out of play (not responding to keyboard commands and remaining stationary). The “waiting” tetrimino will be brought into play at the right moment and replaced by another waiting tetrimino. There’s nothing complicated in terms of the logic.

I begin by positioning two empty game objects in the scene: the first at the top of the grid, centered (Spawn), and the second in the margin between the score display and the title (SpawnProchain). Then, I ask Chat GPT to create a “playing” state for the tetrimino and write the script that achieves what was explained in the previous paragraph, within the Game Manager, of course. We don’t forget to call this function at the start of the game. Once again, the result is quick and elegant.

Create the public boolean EnJeu.
If EnJeu is false, no movement is possible.

Make the Tetrimino array t_Tetriminos private but serialized.
Add two private but serialized GameObject variables: spawn and spawnProchain.
Create the private boolean NouvellePartie = true;
Add public function NouveauTetrimino:
If NouvellePartie is true:Instantiate two random Tetriminos from t_Tetriminos
The first is instantiated at the spawn location and its boolean EnJeu is set to true
The second is instantiated at the spawnProchain location and its boolean EnJeu is also set to true
Set NouvellePartie to false
If NouvellePartie is false:Move the Tetrimino at spawnProchain to spawn and set its EnJeu to true
Instantiate a random Tetrimino from t_Tetriminos with EnJeu set to false at the spawnProchain location.

Logically, the appearance of a new tetrimino should occur when the current tetrimino is placed, either on the “floor” of the game grid or on top of another tetrimino. Therefore, I need to determine a “placed” state for the tetrimino: when the tetrimino is placed, it’s no longer in play, and the NouveauTetrimino() function should be called. I could simply call the function when the tetrimino reaches the bottom of the grid and make it out of play (so it no longer moves), but I know that in the next step, I will need to stop the tetrimino and call the function before it reaches the bottom of the grid (when it lands on another tetrimino). It’s better to plan ahead for what’s necessary.

プライベートなブール値 pose = false を追加
ゲームオブジェクトがグリッドの下端に到達したとき:
EnJeu = false
Pose = true
_GMNouveauTetrimino() 関数を呼び出す

I then launch the application, and it starts talking to me harshly:

Programming (or coding, depending on how you call it) ultimately boils down to two simple things: 1) Knowing how to find and read documentation and 2) Understanding error messages. In this case, the NouveauTetrimino() function seems to be invoking an array element that doesn’t exist. So, I look at the faulty line:

Indeed, to transition from the waiting tetrimino on the left of the grid to the tetrimino in play at the top of the grid, Chat GPT assumed that the waiting tetrimino is a child of the spawn point. The only small problem is that it never told Unity that the tetrimino instantiated at the nextSpawn point should be its child. The script is therefore looking for an element in the array of children of nextSpawn, which is empty (if you didn’t understand this, it’s because you haven’t read Unity’s documentation properly!). Chat GPT’s first failure, which will be punished! In the meantime, I figured it was easier to fix the logic manually rather than trying to phrase my request to Chat GPT. So, I cheated and modified the script directly, but that’s our little secret!

The final step of this part is managing the stacking of Tetriminos. In Tetris, a Tetrimino freezes when one of its blocks “touches” a block below it, allowing for the creation of empty spaces, sometimes closed off, which are at the heart of the gameplay. Therefore, it is necessary to keep track of the positions of the four squares of each placed Tetrimino. To do this, the most efficient method — the one originally used by Alexey Pajitnov — is the two-dimensional array, also called a matrix. The basic principle is quite simple: the game grid is made up of 10×20 cells (200 in total — and I didn’t even need a calculator!) that go from 0,0 (bottom left) to 9,19 (top right). Note that this numbering is valid because, following Valem’s advice, I moved the bottom-left corner to position 0,0. In short, since the Tetrimino moves in complete units, the blocks that compose it are always “in” one of the cells of the grid. The “classic” programming for Tetris is entirely matrix-based, but ours is hybrid: the matrix is not used for movement, but it will be necessary to manage the “collisions” between Tetriminos. To put it simply, I need to ask ChatGPT to create a two-dimensional array representing the grid, where all the cells are empty. Then, it will need to fill in the cells of this array corresponding to the cells occupied by the Tetrimino in play, with each move. When the Tetrimino is no longer in play, its last position will be preserved in the array: the following Tetriminos will need to check if the cells targeted by the next move are free. If they are not, the move will be impossible, and if the move is downward, the Tetrimino will be placed and taken out of play. The grid is managed by the GameManager, and the movement of the Tetrimino based on the grid is handled by the Tetrimino itself. That’s it! Well, that’s the theory. In practice, I struggled a bit to get ChatGPT to do what I wanted. Luckily, I had a model to follow with Valem’s video: I manually corrected the grid created by ChatGPT to make it a Transform array (instead of the integer array the AI suggested), and after several attempts, I found the magic prompt that made ChatGPT understand what I was trying to achieve. But when you know how to talk to him, he’s a good boy!

Create a public 2D array called “Grille”.
Each cell in Grille represents a cell between 0,0 and 9,19.
All cells are empty when Grille is created.

The game grid is represented by the 2D array Grille in _GM.
Add a private function AjoutGrille().
Inside AjoutGrille:
Add the position of each child of the GameObject to _GM.Grille.
Call AjoutGrille() when the GameObject’s downward movement is blocked.
In the IsWithinGrid function:
Return false if any of the cells in _GM.Grille occupied by the GameObject’s children are not null.

With this function, Bob has reached his glass ceiling: my attempts at creating a Tetris clone have always ended here, and I’ve never made it past the next step, which is also the final one. It’s high time to grab a pack of Granola (for courage) and face my Nemesis: line deletion!

Deleting the complete line

On paper, the process is as follows: when the tetrimino is placed, we check if a line has been formed. To do this, we need to go through all the rows from top to bottom and check if any row contains a null cell. If so, that row does not form a line. Otherwise, the row should be cleared (set all cells in that row to null), and all rows above it should be shifted down by one. I’ll apply Valem’s method for this, and I will give these instructions to Chat GPT accordingly.

Create a private function verifierLigne() called when the downward movement is blocked.
Inside verifierLigne():
Go through all the lines of _GM.Grille from the top; if no element in the line is null, call the private functions effacerLigne() and abaisserLignes().
Inside effacerLigne():
Set all the elements of the line to null.
Inside abaisserLignes():
Move all lines above the erased line down by one unit.

I launch the game and everything works… badly T_T. The solution proposed by Valem, faithfully replicated by Chat GPT, doesn’t account for or poorly handles the simultaneous deletion of multiple lines: after the first line is cleared, the deleted tetriminos are no longer in play but still appear, causing the other tetriminos to pass through, and making the game unreadable. I immediately identified the cause of the problem because I’ve spent quite a few hours with Unity: the visual update of the grid doesn’t work correctly when more than one line is deleted at the same time. There are two possible solutions: 1) Try to optimize the algorithm so that the calls are made in the correct order or 2) Fix the problem crudely by using a tag attached to each block of the tetriminos… Guess which option Bob, the king of laziness, chose!

In effacerLigne(), delete all objects with the “Bloc” tag located on the line.

And then… magic! Bob has defeated his nemesis and programmed a functional Tetris! Joy and happiness… Breaking down the logic into small functions, as shown in Valem’s tutorial, allowed me to avoid the convoluted functions that ended up getting stuck in previous attempts. Simplicity is often the best path in programming. However, a few small improvements wouldn’t hurt.

Improvements

The game logic is in place, and the small improvements I want to make are simple but should enhance the gameplay experience:

  • Visual and sound feedback when a line is cleared
  • Background music and sound feedback when rotating and landing a Tetrimino
  • Score update, level progression, and speed increase
  • GameOver management and sound feedback
  • Rotation with the spacebar (in addition to the up arrow key)

I start by importing background music, along with four audio clips for rotation, piece placement, line removal, and game over. Then it’s Chat GPT’s turn to finish the job, which it does almost flawlessly. The only issue is that it’s a bit lazy with the visual feedback for line removal (it creates an empty function and invites me to complete it…), so I quickly hack something together in one line; I also take the opportunity to increment the line count in the Game Manager each time a line is removed, something I forgot to ask the AI for.

Add:
The piece can also be rotated using the spacebar
Play the “Rotation” sound during rotation
Play the “Placement” sound when the movement is blocked downwards
Play the “Deletion” sound when a line is removed
Display a white flash across the entire line when a line is removed
Add 10 * _GM.Niveau to _GM.Score and call _GM.ActualiserUI() when a line is removed

Managing the Game Over is simple: we check if a block is below the tetrimino. If it is, we stop the game and display the game over screen; I then add playing a sound by directly modifying the obtained script.

At the beginning of each Update(), if EnJeu is true, check if any of the GameObject’s children are at the same position as a non-null cell in the grid.
If that’s the case, call _GM.GameOver().

Finally, I slightly modify the Game Manager to increase the level (and speed) every 10 lines.

So, I’ve finally got a playable and somewhat finalized Tetris. I could continue to improve it, but that’s not the goal of my experience, and now it’s time to draw the lessons from it.

Conclusion

What lessons can be drawn from this experience? First, and not surprisingly, Artificial Intelligence is not intelligent. It’s a very interesting tool that can make a good assistant, but in the end, it’s just an enhanced search engine. All the thinking has to come from the human controlling the AI. In the case of Tetris, my previous attempts with AI always led me to a dead end. It took me being inspired by another human’s work—the video by Valem—to understand my mistakes and be able to get what I wanted from Chat GPT: Chat GPT never corrected my logical errors. That said, once the concept is clear, it is much faster and more efficient to get a script written by Chat GPT than to dig through documentation to build your own. In fact, since I’ve been programming with the help of AI toolbox for just over a year, I’ve learned a lot: by expressing my needs in natural language to the AI, it provides me with scripts that, while they don’t always work on the first try, use techniques that would have been impossible for me to find in documentation, simply because I couldn’t name the concepts correctly. In addition to being a fast typist that makes no typos, Chat GPT is also an effective vector for discovery. However, there’s a catch: to benefit from AI in programming, you first have to know how to program. Without that, it’s impossible to evaluate the suggested scripts, understand the blocking errors, and fix them. And just like a good worker is one who knows how to handle his tools, the good AI-assisted programmer is one who knows how to write prompts so Chat GPT interprets them correctly. And that’s not an easy task: I had to rephrase at least a third of the requests in this article to get the desired result, even though I’ve been using the tool for over a year. While being a good executor, AI still needs a human above it to design and verify its work. In short, and as I’ve written before, it’s not Artificial Intelligence that will steal your job, but the humans who know how to use it. To those who understand, you’ll get the message.

Bob Dupneu

Bob invites you to try below the result of his experiment, TetrIA (click on the window to activate it).

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.