Using AI to make a 3D tic-tac-toe game
You have a great idea for a cool web application people would enjoy using. Unfortunately, you don’t know any programming. Have no fear, AI is here to help you. In this article series we’ll build a 3d tic tac toe game together. We will need some programming concepts, but I’ll explain when as needed. I assume you know how to use Windows, and that’s about it.
This article focuses on creating a user interface. We build a web page that implements a two player game, as long as both players sit next to the same browser.
I’m going to provide everything I used to create the game, so you can follow along. However, remember AI is not deterministic. Even if you use the same prompts I’m using while writing this, on the same model, the results may vary. And of course, now that you’re reading it, days and months later, the model has probably been updated. The important parts should be similar enough for the directions to be useful, but don’t expect perfection.
Preparation
-
Download and install Visual Studio Code ↗. It comes integrated with Microsoft Copilot ↗.
-
After it starts, click the agent icon in the top right.

-
Click New session > Select and select a directory or create a new one.
-
Click Trust Folder & Continue. Select Copilot CLI as the model.
-
We are going to use Node ↗. Use this prompt to tell Copilot to install it.
Make sure node is properly installed and configured on this system. -
When asked for permission to run a command, click Allow > Allow All Commands in the Session.
What we are building
There are three 3x3 boards, and the players take turns. Instead of ending the game as soon as somebody gets a row, which in this case would always be the first player, each row is a point. The game ends when there are no free spots left.
Regular tic-tac-toe
As a first approximation, we’ll build a regular tic-tac-toe game for two players. There is probably already such a game in the data the model ingested, since it is so common. So creating the game should be easy.
Use this prompt:
I want you to create a web page that lets two players play tic-tac-toe.
Chat response
Done! I’ve created a fully functional two-player tic-tac-toe web page. Here’s what it includes:
Features:
- Turn indicator - Shows whose turn it is (Player X or Player O)
- Interactive board - 3x3 grid where players click to make moves
- Win detection - Automatically detects wins and draws
- Score tracking - Keeps track of wins for both players and draws
- Reset button - Start a new game anytime
- Responsive design - Clean, modern UI with purple gradient background
How to play:
- Open
index.htmlin any web browser - Player X goes first
- Players alternate clicking empty cells
- The game automatically detects wins (three in a row/column/diagonal) or draws
- Use the “Reset Game” button to start a new round
- The score persists between games
The game is self-contained in a single HTML file with embedded CSS and JavaScript—no dependencies needed!
Supporting multiple lines
In regular tic-tac-toe the game is over and victory is declared as soon as a line is complete, but in this version we want to allow multiple lines. We can do it using this prompt:
Change this game so that each time a player completes a line that player just gets a point (instead of winning the game) and the game continues until there are no more empty spaces.
Chat response
Done! I’ve updated the tic-tac-toe game with the new rules:
Changes:
- Continuous play - When a player completes a line, they get 1 point but the game continues
- Full board required - The game only ends when all 9 spaces are filled
- Points display - Shows current points during the game in the center
- Game winner - Whoever has the most points when the board fills wins that game
- Game tracking - Tracks the number of games won by each player and ties at the bottom
How it works now:
- Players alternate turns as before
- Each time they complete a line (horizontal, vertical, or diagonal), they get +1 point
- The game message shows which player completed a line and their new point
- After all 9 cells are filled, the game ends and shows the final score
- Use “Reset Game” to play again
Made changes.
The third dimension
Now that the game works in two dimensions, let’s try three.
I want the game three dimensional. Have three 3x3 boards, and a straight line going vertically, or a diagonal with one of the directions being vertical, also counts as a line.
Chat response
Perfect! I’ve successfully converted the tic-tac-toe game to 3D. Here’s what changed:
Key Features:
- Three 3x3 boards displayed side-by-side (Layer 1, 2, and 3)
- 27 cells total (0-26) arranged in a 3x3x3 cube
- Comprehensive line detection including:
- All horizontal and vertical lines within each 2D layer (24 lines)
- Straight vertical lines through all 3 layers at the same position (9 lines)
- Diagonal lines within each layer (6 lines)
- Diagonal lines across layers in the x-z plane (6 lines)
- Diagonal lines across layers in the y-z plane (6 lines)
- Full 3D diagonal lines through the entire cube (4 lines)
Game Rules:
- Players alternate turns filling cells
- Each time a player completes a line, they earn 1 point
- The game continues until all 27 cells are filled
- The player with the most points wins that game
- Scores are tracked across multiple games
Try it out! The game is fully playable with all 3D line detection working.
Made changes.

It works, but the connection between different levels is not very clear. Let’s try to fix it.
Try to rotate the boards 45 degrees and stack them one on top of the other.
Chat response
Perfect! I’ve updated the 3D tic-tac-toe game with the following changes:
Visual Improvements:
- Rotated boards - Each board is now rotated 45 degrees on the Z-axis (horizontal tilt) and tilted 25 degrees on the X-axis (depth perspective)
- Stacked vertically - The three boards are now stacked one on top of the other with depth separation, creating a true 3D isometric view
- 3D perspective - Added CSS perspective and transform-style to create proper 3D depth
- Enhanced shadows - Added drop shadows to make the depth more apparent
The boards now appear as an isometric 3D cube structure, with each layer visually separated and positioned in 3D space. This gives a much better sense of the three-dimensional tic-tac-toe board!
Made changes.

Now the 3d nature of the game is clear, but the boards overlap; the top corner of the bottom board is hidden, as is the score display Additionally, you can’t click the top corner of the top board.
The top corner of the bottom board is hidden by the middle board. The score display is hidden by the bottom board. Also, it is impossible to click the top corner of the top board and the two squares that touch it. Fix both issues.
Chat response
Done! I’ve fixed both issues:
Issue 1 - Hidden elements:
- Increased the gap between boards from 60px to 200px
- Changed the z-axis translation to move boards back instead of forward (negative translateZ values)
- Added explicit z-index values so top board is in front, middle board behind it, bottom board in back
- Increased bottom margin of boards container from 30px to 80px
- Moved score display further down with 100px top margin
Issue 2 - Unclickable cells:
- Added
pointer-events: autoto cells to ensure they’re clickable - Added
position: relativeandz-index: 1to cells so they stay above the board background - Adjusted the 3D transforms to better separate layers without overlapping click areas
The boards should now be properly spaced with no visual overlap, all cells should be clickable, and the score display is fully visible below the boards.
Made changes.

This is even worse! Many more spaces are unclickable, and there is too much space between the boards, making it difficult to get a holistic view of the game. Also, we don’t need the layer headings.
- Remove the “layer
” headings. - Move the boards closer together so the web page can show all three at once.
- Make sure every square is clickable. Now even more of them are not.
Chat response
Done! I’ve fixed all three issues:
-
Removed “Layer” headings - Set
display: noneon.board-label -
Moved boards closer together - Reduced gap from 200px to 20px so all three boards fit on screen
-
Fixed clickability - Made several changes:
- Removed
transform-style: preserve-3dwhich was interfering with pointer events - Removed problematic
pointer-events: autodeclarations - Enhanced the JavaScript click handler to use
.closest()to ensure it properly detects clicks even with CSS transforms - Cleaned up transitions to only animate background and color (avoiding transform animations that can interfere with clicks)
- Removed
The boards are now stacked closer together and all 27 cells should be fully clickable!
Made changes.
This is much better. All 27 cells are clickable (you can verify this yourself) and the 3D nature is visible. However, there is a way to make it even clearer.
When I mouse over a cell, can you change the color of all the cells that make a line with it?
Chat response
Perfect! I’ve added line highlighting on hover. Here’s what happens:
When you mouse over a cell:
- All cells that form a line with that cell are highlighted in bright yellow with a glowing shadow
- This shows you all possible lines that could be completed by placing pieces along with that cell
Visual feedback:
- Highlighted cells get a bright yellow background (
#ffeb3b) - A subtle glowing shadow effect makes them stand out
- When you move your mouse away, all highlights disappear
The highlighting works for all types of lines:
- Horizontal and vertical lines in each layer
- Diagonal lines within layers
- Vertical lines through all 3 layers
- Cross-layer diagonals
- 3D diagonals
This makes it easy to strategically see which cells are part of the same lines as the cell you’re hovering over!
Made changes.
Debugging

The new change indicates an error. This screen capture was taken when the cursor was in the square marked with red. The “O”s form a diagonal line that the application should recognize, but doesn’t. Let’s try to tell the AI to fix it.
You don’t recognize some of the diagonals. For example, if we put the z co-ordinate as the board with 0 being the lowest and 2 the highest, you do not recognize
(0,0,0),(1,0,1),(2,0,2). Fix this.
Chat response
Perfect! I’ve fixed the missing and incorrect diagonal lines. The corrections were:
Fixed xz plane diagonals (y constant):
- Row 0:
[2, 10, 18]instead of[2, 12, 22]- this gives you the line (2,0,0)→(1,0,1)→(0,0,2) ✓ - Row 1:
[5, 13, 21]instead of[5, 15, 21]- correctly represents the backward diagonal - Row 2:
[8, 16, 24]instead of[8, 14, 20]- this gives you the line (2,2,0)→(1,2,1)→(0,2,2) ✓
The line you mentioned (0,0,0)→(1,0,1)→(2,0,2) which is [0, 10, 20] was already there, but the backward diagonals and some other orientations were wrong. Now all 49 possible lines (9 + 9 + 6 + 9 + 6 + 6 + 4) should be correctly recognized and highlighted when you hover!
Made changes.
The next step is to mouse over all the squares and see that in all cases all the lines are covered. However, while playing the game, I noticed a different bug. The scoring is wrong.

In this game, X managed to make only a single line (the one at the top corner of each board). However, the score is two. Maybe there is a sequence of squares that the application mistakenly treats as a line.
Both this problem and the previous one are related to the squares. To explain them, it would help if we could communicate which squares are which to the AI.
Write on each square the square number
Chat response
Perfect! I’ve added square numbers to each cell. Here’s what changed:
Updates:
- Added square numbers - Each square now displays its number (0-26) in small gray text at the top
- Restructured HTML - Each cell now contains two spans:
.cell-number- displays the square number (always visible).cell-content- displays the X or O when a player makes their move
- Updated styling - Cells now use flexbox to stack the number above the X/O mark
- Updated JavaScript - Modified the click and reset functions to update only the
.cell-contentspan, preserving the square numbers
Now you can easily reference squares by their numbers when playing!
Made changes.
We don’t know where exactly the application thinks there’s a line, so we’ll ask.
Can you give me all the triplets you recognize as lines?
Chat response
Here are all 49 triplets the game recognizes as valid lines:
Horizontal lines in each layer (9 lines):
- [0, 1, 2], [3, 4, 5], [6, 7, 8] — Layer 0
- [9, 10, 11], [12, 13, 14], [15, 16, 17] — Layer 1
- [18, 19, 20], [21, 22, 23], [24, 25, 26] — Layer 2
Vertical lines within each layer (9 lines):
- [0, 3, 6], [1, 4, 7], [2, 5, 8] — Layer 0
- [9, 12, 15], [10, 13, 16], [11, 14, 17] — Layer 1
- [18, 21, 24], [19, 22, 25], [20, 23, 26] — Layer 2
Diagonals within each layer (6 lines):
- [0, 4, 8], [2, 4, 6] — Layer 0
- [9, 13, 17], [11, 13, 15] — Layer 1
- [18, 22, 26], [20, 22, 24] — Layer 2
Vertical lines through all layers (9 lines):
- [0, 9, 18], [1, 10, 19], [2, 11, 20] — Column 0
- [3, 12, 21], [4, 13, 22], [5, 14, 23] — Column 1
- [6, 15, 24], [7, 16, 25], [8, 17, 26] — Column 2
Diagonals in xz plane (6 lines):
- [0, 10, 20], [2, 10, 18] — Row 0
- [3, 13, 23], [5, 13, 21] — Row 1
- [6, 16, 26], [8, 16, 24] — Row 2
Diagonals in yz plane (6 lines):
- [0, 12, 24], [6, 12, 18] — Column 0
- [1, 13, 25], [7, 13, 19] — Column 1
- [2, 14, 26], [8, 14, 20] — Column 2
3D body diagonals (4 lines):
- [0, 13, 26], [2, 13, 24], [6, 13, 20], [8, 13, 18]
Total: 49 lines
The X squares in the game are 0, 9, 18, and 19. Unfortunately, only one of those is in the list of lines, [0, 9, 18]. We don’t know why the score is two, so let’s ask.
I have four X squares in the game: 0, 9, 18, and 19. Why does it show X having a score of two?
Chat response
I found the bug! The issue is that the checkForLines() function recalculates all completed lines every time a move is made, without tracking which lines were already scored in previous turns.
Here’s what happened:
- When you placed X at 18, the line [0, 9, 18] was completed → +1 point
- When you placed X at 19, the code checked for lines again and re-scored [0, 9, 18] → +1 point (incorrectly!)
The function needs to track which lines have already been scored so they don’t get counted twice. Let me fix this: … Fixed! The bug was that the code was recounting completed lines on every move. Here’s what was happening:
Before the fix:
- When you placed X at 18, the line [0, 9, 18] was scored → +1 point
- When you placed X at 19, the function checked all lines again and re-scored [0, 9, 18] → +1 point (incorrectly!)
After the fix:
- I added a global
completedLinesSet that tracks which lines have already been scored - Now when a line is completed, it’s added to this set
- On future moves, the code only scores new lines that haven’t been counted before
- The set is cleared when you reset the game
With squares 0, 9, 18, and 19, you should now correctly see a score of 1 (only the [0, 9, 18] line).
Made changes.
The AI found the problem and fixed it, or at least it appears so. Since it found one bug, it might be able to find others.
Can you find other bugs?
Chat response
Let me review the code for potential bugs:
…
I found a bug! In handleCellClick, after setting contentSpan.textContent = currentPlayer, we also do cell.classList.add(currentPlayer.toLowerCase()). But the .x and .o classes are styled to change text color. Since we’re now displaying X or O in the .cell-content span, the color styling won’t work correctly because the class is on the button, not the text span…
.
Done! I found and fixed this bug:
Bug: Incorrect color styling for X and O
- The
.xand.oCSS classes were being added to the button instead of the text span - This meant the text color wasn’t changing to blue for X and purple for O
- Fix: Now the color classes are applied to the
.cell-contentspan which actually contains the text
The game should be solid now. The main things working correctly:
- ✅ All 49 lines detected properly
- ✅ Lines don’t get re-scored
- ✅ Hover highlighting works
- ✅ All cells are clickable
- ✅ Square numbers display correctly
- ✅ X and O now display in their proper colors
Made changes.
I tried the same again, and it found another bug.
Can you find other bugs?
Chat response
Updated todo list
Let me analyze the code systematically for potential bugs:
…
I found a bug! Look at line 390:
if (gameBoard[a] === gameBoard[b] && gameBoard[a] === gameBoard[c] && gameBoard[a] === currentPlayer)This checks if gameBoard[a] === currentPlayer, but what if a cell contains ‘X’ or ‘O’ from a previous move, and we’re comparing it with currentPlayer?
The bug: If a line has mixed markers (some X, some O, or some empty), we check gameBoard[a] === currentPlayer, but we should also verify it’s actually ‘X’ or ‘O’, not an empty string. Actually wait - we already skip lines with empty cells on line 387-388, so gameBoard[a], gameBoard[b], and gameBoard[c] are all guaranteed to be non-empty. So that’s not a bug.
Let me check the hover functions:
…
Found it! In handleCellHover (line 346), there’s a potential bug:
document.querySelector(`[data-index="${cellIndex}"]`).classList.add('highlight');If document.querySelector() returns null (which shouldn’t happen, but could if the HTML is malformed), calling .classList.add() on null would crash the game with an error.
Also, repeatedly calling querySelector in a loop is inefficient. Let me fix this:
…
Fixed! The bug was:
- Unsafe DOM query - If
querySelectorreturnednull, the code would crash trying to call.classList.add()on null - Added null check - Now we verify the cell exists before trying to add the highlight class
This is a defensive programming fix that prevents potential crashes, even though the querySelector should normally always find the element.
Made changes.
Burning tokens
I thought there was no real reason for a human in this loop, so I entered this prompt:
Can you repeatedly look for bugs until you can’t find anymore?
It found 27 more bugs, and stopped not because it ran out of bugs, but because it ran out of credits.
Conclusion
You should now be able to implement whatever user interface you need. This is great for two-player games where the players are in the same place. But in most cases a web application does more than that. It lets people communicate, using some kind of internal database. This requires a server.
In the next installment in this series, I teach you how to add such a server.