Termloop
Terminal-based game engine for Go, built on top of Termbox
Install / Use
/learn @JoelOtter/TermloopREADME
Termloop

Termloop is a pure Go game engine for the terminal, built on top of the excellent Termbox. It provides a simple render loop for building games in the terminal, and is focused on making terminal game development as easy and as fun as possible.
Termloop is still under active development so changes may be breaking. I add any breaking changes to the Changelog - hopefully at this stage there shouldn't be too many. Pull requests and issues are very welcome, and do feel free to ask any questions you might have on the Gitter. I hope you enjoy using Termloop; I've had a blast making it.
Installing
Install and update with go get -u github.com/JoelOtter/termloop
Features
- Keyboard and mouse input
- Collision detection
- Render timers
- Level offsets to simulate 'camera' movement
- Debug logging
- Built-in entity types such as:
- Framerate counters
- Rectangles
- Text
- Loading entities from ASCII art
- Loading colour maps from images
- Loading level maps from JSON
- Optional 'pixel mode' - draw two 'pixels' to a terminal character, doubling screen height at the expense of being able to render text.
- Pure Go - easy portability of compiled games, and cross-compilation built right in.
To see what's on the roadmap, have a look at the issue tracker.
termloop/extra
The Termloop extras are a collection of types and functions, the use of which will not result in a fully portable binary - that is, they have some external dependencies. However, if you're willing to require these dependencies in your project, they should integrate quite nicely with the rest of Termloop. Some of the included examples use these extras.
- Audio playback
- audio.go
- Requirements: PortAudio and libsndfile
Cool stuff built with Termloop
- Included examples (@JoelOtter)
- Number Crusher (@aquilax)
- Go Tapper (@swapagarwal)
- Frame Assault (@Ariemeth)
- Minesweeper (@ryanbaer)
- Termtank (@TerrySolar)
- Snake (@mattkelly)
- Go Man's Sky (@rawktron)
- conwaygo (@buckley-w-david)
- Doric (a Columns clone) (@svera)
- Terminal-based Snake (@tristangoossens)
- Sokoban (@tristangoossens)
- Gopher Typer (@scottbrooksca)
- Tetris (@cam73)
- Gorched (@zladovan)
- Go Invaders (@afagundes)
Feel free to add yours with a pull request!
Tutorial
More full documentation will be added to the Wiki soon. In the meantime, check out this tutorial, the GoDoc, or the included examples. If you get stuck during this tutorial, worry not, the full source is here.
Creating a blank Termloop game is as simple as:
package main
import tl "github.com/JoelOtter/termloop"
func main() {
game := tl.NewGame()
game.Start()
}
We can press Ctrl+C to exit. It's just a blank screen - let's make it a little more interesting.
Let's make a green background, because grass is really nice to run around on. We create a new level like so:
level := tl.NewBaseLevel(tl.Cell{
Bg: tl.ColorGreen,
Fg: tl.ColorBlack,
Ch: 'v',
})
Cell is a struct that represents one cell on the terminal. We can set its background and foreground colours, and the character that is displayed. Creating a BaseLevel in this way will fill the level with this Cell.
Let's make a nice pretty lake, too. We'll use a Rectangle for this. We'll put the lake at position (10, 10), with width 50 and height 20. All measurements are in terminal characters! The last argument is the colour of the Rectangle.
level.AddEntity(tl.NewRectangle(10, 10, 50, 20, tl.ColorBlue))
We don't need to use a Level - we can add entities directly to the Screen! This is great for building a HUD, or a very simple app. However, if we want camera scrolling or collision detection, we're going to need to use a Level.
Putting together what we have so far:
package main
import tl "github.com/JoelOtter/termloop"
func main() {
game := tl.NewGame()
level := tl.NewBaseLevel(tl.Cell{
Bg: tl.ColorGreen,
Fg: tl.ColorBlack,
Ch: 'v',
})
level.AddEntity(tl.NewRectangle(10, 10, 50, 20, tl.ColorBlue))
game.Screen().SetLevel(level)
game.Start()
}
When we run it with go run tutorial.go, it looks like this:

Pretty! Ish. OK, let's create a character that can walk around the environment. We're going to use object composition here - we'll create a new struct type, which extends an Entity.
To have Termloop draw our new type, we need to implement the Drawable interface, which means we need two methods: Draw() and Tick(). The Draw method defines how our type is drawn to the Screen (Termloop's internal drawing surface), and the Tick method defines how we handle input.
We don't need to do anything special for Draw, and it's already handled by Entity, so we just need a Tick:
type Player struct {
*tl.Entity
}
func (player *Player) Tick(event tl.Event) {
if event.Type == tl.EventKey { // Is it a keyboard event?
x, y := player.Position()
switch event.Key { // If so, switch on the pressed key.
case tl.KeyArrowRight:
player.SetPosition(x+1, y)
case tl.KeyArrowLeft:
player.SetPosition(x-1, y)
case tl.KeyArrowUp:
player.SetPosition(x, y-1)
case tl.KeyArrowDown:
player.SetPosition(x, y+1)
}
}
}
Now that we've built our Player type, let's add one to the level. I'm going to use the character '옷', because I think it looks a bit like a stick man.
player := Player{tl.NewEntity(1, 1, 1, 1)}
// Set the character at position (0, 0) on the entity.
player.SetCell(0, 0, &tl.Cell{Fg: tl.ColorRed, Ch: '옷'})
level.AddEntity(&player)

Running the game again, we see that we can now move around the map using the arrow keys. Neato! However, we can stroll across the lake just as easily as the grass. Our character isn't the Messiah, ~~he's a very naughty boy,~~ so let's add some collisions.
In Termloop, we have two interfaces that are used for collisions. Here they are.
// Physical represents something that can collide with another
// Physical, but cannot process its own collisions.
// Optional addition to Drawable.
type Physical interface {
Position() (int, int) // Return position, x and y
Size() (int, int) // Return width and height
}
// DynamicPhysical represents something that can process its own collisions.
// Implementing this is an optional addition to Drawable.
type DynamicPhysical interface {
Position() (int, int) // Return position, x and y
Size() (int, int) // Return width and height
Collide(Physical) // Handle collisions with another Physical
}
It's pretty simple - if we want our object to be 'solid', then we implement Physical. If we want a solid object that actually does some processing on its own collisions, we implement DynamicPhysical! Essentially this just involves adding one more method to your type.
Note that, for performance reasons, you should try and have as few DynamicPhysicals as possible - for example, our Player will be one, but the lake need only be a Physical.
The Rectangle type already implements Physical, so we don't actually need to do anything. As well, Player already implements DynamicPhysical because of the embedded Entity. However, we want custom behaviour for Collide, so let's implement that method. For that, we'll have to modify our struct and Tick method, to keep track of the Player's previous position so we can move it back there if it collides with something.
type Player struct {
*tl.Entity
prevX int
prevY int
}
func (player *Player) Tick(event tl.Event) {
if event.Type == tl.EventKey { // Is it a keyboard event?
player.prevX, player.prevY = player.Position()
switch event.Key { // If so, switch on the pressed key.
case tl.KeyArrowRight:
player.SetPosition(player.prevX+1, player.prevY)
case tl.KeyArrowLeft:
player.SetPosition(player.prevX-1, player.prevY)
case tl.KeyArrowUp:
player.SetPosition(player.prevX, player.prevY-1)
case tl.KeyArrowDown:
player.SetPosition(player.prevX, player.prevY+1)
}
}
}
func (player *Player) Collide(collision tl.Physical) {
// Check if it's a Rectangle we're colliding with
if _, ok := collision.(*tl.Rectangle); ok {
player.SetPosition(player.prevX, player.prevY)
}
}
Not too muc
Related Skills
node-connect
349.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
xurl
349.2kA CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.
frontend-design
109.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
