Building Tetris with GitHub Copilot
After receiving an invite for the GitHub Copilot Technical Preview I decided to try out the feature by building a browser-based Tetris-clone. This post will look at how Copilot offers suggestions while building software.
The completed result is below, and you can see the source here.
Approach
I’ll start from a blank slate and am looking to build a working app as quickly as possible. I won’t look to optimise the code for performance - rather I’m looking at a first pass that feels similar to the famous block game. I’ll use TypeScript for code and pixi.js for rendering, and hopefully Copilot will write a bunch of the code for me.
I install and authorise the vscode extension for Copilot, create a new vite project, and open the IDE:
npm init vite@latest tetris --template vanilla-ts
cd tetris
npm i pixi.js
code .
From the scaffolded main.ts
that vite creates, I remove the app.innerHTML
setter, and rename #app
to #pixi
. My starting point looks like this:
import "./style.css";
const element = document.querySelector<HTMLDivElement>("#pixi")!;
The First Suggestions
In the screenshots that follow, grey text is a suggestion from GitHub Copilot. I type const
and immediately Copilot gives me the first suggestion:
This suggestion is impressive for a number of reasons:
- It’s correct - I was going to write that
- The file does not yet have imports for pixi.js
- The correct module alias is suggested:
PIXI
- The correct class name is suggested:
Application
Hitting tab accepts the suggestion. I know I want pixi to resize based on its parent element, so intellisense gives me resizeTo
as I type it but upon selecting it I get another suggestion from Copilot:
This is also correct, and, references the existing variable. Hitting tab to accept the suggestion completes the line, but this is not executable code because an import
is still required. I move to the top of the file, type import * as PIXI
and another correct suggestion is offered:
Pondering whether Copilot was clever enough to inspect package.json
or node_modules
I tried the above steps again on a new repo without installing pixi. I get the same correct suggestions, and intellisense on the PIXI.Application
options, even though the package is not installed - the screenshot below has a red squiggly under pixi.js
because it’s not installed.
I go to the bottom of the file, hit enter, and again get a correct suggestion:
While I type, partial lines, and sometimes entire lines, are constantly suggested with striking accuracy. It continually uses and references what I’ve already written and appears to know as much pixi syntax as I do. Were I a pixi beginner, suggestions like app.screen.width
introduce me to API that otherwise can take some time to search for and learn. So far, I’m quite impressed.
Tetrominos
The player pieces in Tetris are called Tetrominos, so I’ll create an abstract class that represents their colour and shape and create a concrete class for each of the seven block types I, O, T, S, Z, J, and L. I create a new file, type the name of the class, and I get my first nonsense suggestion:
At this point my pessimism has me expecting a lot more of these. However, when I discard the suggestion and start typing what one of the concrete implementations might look like, I get a startling result where each of the concrete classes is correctly suggested at once:
My intention is to have a static factory method on the base class that will return random Tetrominos. I type static
and it turns out Copilot feels the same way I do:
The next suggestion implements the entire method for me!
Each of the concrete classes need to have their block layout specified in the constructor. Copilot was able to suggest correct block layouts for each of the seven Tetrominos, however this needed tweaking afterwards as some used -1
and others 0
as the lowest y
value.
On the base class, moveLeft
, moveRight
, and moveDown
were correctly named and implemented via a sequence of pressing Enter
followed by Tab
:
Although I took a different approach later, I originally implemented a TetrominoView
class responsible for drawing Tetrominos. Copilot suggested a constructor with using Parameter Properties - a feature I was unaware that TypeScript had. It additionally suggested a very accurate draw
function. The only part I needed to change was to translate the sizes to pixels:
Writing Tests
With Tetrominos being created and drawn, I want to implement the rotate
method. Geometry is not as brainless as bashing keys so I’ll write tests to help with this feature. I added jest to the project and started writing the first test. Copilot suggested an accurate, but otherwise low-value test:
My intention was to have Copilot write rotate
for me, but initial suggestions were verbose and inaccurate, and admittedly it took me too long to remember you don’t need sin
/cos
when you’re rotating 90 degrees. Eventually, with a comment added to help suggestions, I got a concise result:
Test writing suggestions weren’t great until I had a bit of repetition in the file, after which it could suggest accurate tests even if some of them missed a closing bracket:
Listening For Keys
After typing window.addEventListener
it correctly suggested the event:
And after accepting the suggestion, correctly suggested the implementation for arrow keys:
Moving my cursor to the bottom of the implementation, it then suggested adding game.drop()
, a feature I had not thought of yet! Note the red squiggles on methods that don’t exist yet - they are correct, but yet to be implemented:
When adding the drop
method all I was certain of was that I’d add a new block at the end. So I added that, moved the cursor above and Copilot suggested the rest of the implementation correctly one line at a time:
Suggestion Accuracy
After spending around eight hours building the game and messing around with Copilot I can comment on both its strengths and weaknesses. Ultimately it comes down to how frequent and accurate its suggestions are. Most social media screenshots I’ve seen are of nonsense suggestions, but I only had one or two occasions when that occurred during development, and it only takes a second to laugh move on.
When suggesting full functions I found it could miss a bracket - especially in tests. But, adding a single bracket after accepting a suggestion is still quicker than typing the suggestion myself. It will suggest modules that are not yet imported
and, accepting the suggestion will not add an import
- however a CTRL+.
afterwards can Quickfix this. It doesn’t offer suggestions during multi-cursor activities. I use prettier to formatOnSave
and doing so sometimes has Copilot suggest duplicate lines that already exist but have been formatted across lines. It also gets whitespace wrong a bunch, but formatOnSave
fixes that anyway.
The frequent and, sometimes disturbingly, accurate partial and full-line suggestions are amazing time savers. The discoverability of new API and language features while typing code means you can learn like you’re pair programming. The occasional full function implementation is just icing on an already delicious cake.
Importantly, suggestions don’t rely on existing code to be syntactically correct. The code doesn’t have to build before accurate suggestions are available. I don’t need the correct import
s to be present. This means the tool can immediately slip into my way of coding - add some lines here, move up or down, tweak this, write half a line here, it doesn’t matter - the suggestions keep coming through.
Conclusion
Overall I’m very impressed with Copilot and quite happy to include it as part of my development toolset. I chose a project for this post that I presume has plenty of implementations across public repos, and Copilot’s knowledge of Tetris does appear to validate this presumption. I’m looking forward to seeing how it will perform across other project types and codebases in the future.