Recreating the CyberPunk 2077 minigame
The game CyberPunk 2077 features a minigame called “Breach Protocol” where you
hack into control systems like traffic lights and home automation controllers.
The hacking screen features a square grid of hexadecimal numbers, and the goal
is to enter one or more given sequences of codes into the code buffer.
It is a game-y depiction of hacking, but I guess it succeeds, as solving
the puzzles is quite fun. Just as an exercise, I wanted to recreate the
minigame so I can play and practice whenever.
The rules of the game need some explaining. You have a code buffer with
(in the beginning) only four positions. You enter codes into the buffer
by selecting them from the code matrix. The goal is to enter at least
one of the required sequences. A major gotcha is that you can’t just select
any code from the code matrix; you have to alternate between choosing from
the row, column. So starting from the top row, we can pick from
D0-45-93-65-6A
. After punching in the code 6A
, we can pick from
the rightmost column []-6A-93-65-D0
. Punch in 45
, and next we select from
the row 93-6A-45-65-[]
. So, you have to ‘snake’ your way across the grid.
Once you get the hang of it, the game becomes pretty easy; it’s just a matter of looking up the right numbers. The real challenge however is entering all three sequences through cleverness. A three-way solution is not always possible, but it does occur. Winning big earns you extra buffer slots. At later stages, the code matrix increases in size to 6x6, and the code sequences become longer.
CyberPunk 2077 is a professional game with great graphics and animations,
while my own version is terminal based. For drawing the screen I’m using
the tcell
module for Go. Now, tcell
is about as barebones as it can get.
Want to print text to the screen? You shall put the characters one by one,
as if plotting pixels into a framebuffer. It is text mode however, so
like with ncurses
, characters have a fixed pair of foreground and
background color (256-color mode is supported, but this also requires
terminal support). If you just want to change the color, you are going to
have to redraw the text.
It feels clunky and old-fashioned, but I love the “less is more” approach,
it’s abolutely glorious.
textStyle := tcell.StyleDefault.Foreground(tcell.ColorLightCyan).Background(tcell.ColorBlack)
screen.setStyle(textStyle)
// drawText renders a string one character cell at a time
func drawText(x, y int, text string, style tcell.Style) {
for _, c := range text {
screen.SetContent(x, y, c, nil, style)
x++
}
}
Using tcell
is a lot like using SDL or Raylib in that it uses an event loop
for keyboard input, and then allows you to update and render the screen.
func gameUpdate() {
for screen.HasPendingEvent() {
ev := screen.PollEvent()
if ev == nil {
gameState = GameQuit
break
}
switch ev := ev.(type) {
case *tcell.EventKey:
keyCode := ev.Key()
keyChar := ev.Rune()
handleKeyEvent(keyCode, keyChar)
}
case *tcell.EventResize:
screen.Sync()
}
}
func gameRender() {
// ... draw things (not shown here)
screen.Show()
}
Even mouse events are supported, but didn’t work with my terminal program.
One thing tcell
lacks is a timer event. TUI applications tend to be driven
by keyboard input, and therefore usually don’t have a framerate like
graphical games do. Since this hacking minigame runs on a timer, I implemented
my own timer and added a simulated framerate.
if !hadEvent {
// "frame rate"
time.Sleep(33 * time.Millisecond)
}
if Timer.IsRunning() {
Timer.Update()
}
For this toy game we could have done without a framerate, but I wanted
the code to be structured as a mainloop with update and render.
Having a framerate means doing lots of redraws even when the screen did not
change, and you would expect a lot of screen flicker. I took care to redraw
only parts of the screen that truly did change, but it turns out tcell
already does an excellent job at just that; it’s absolutely flicker-free,
even on slow remote terminal connections.
This post came out being much more about tcell
than the actual game code.
The key to writing game code is keeping the game logic entirely separate from
input/output. Working with tcell
is in fact so much like SDL/Raylib, that
in theory we could swap libraries and turn it into a graphical game.
Is Go good for games? Sure, why not.