Tortuga

note: this project is a work in progress and in early stages

The goal of tortuga is to be able to easily make tiny games. Tortuga is similar to turtle. However, several components have been stripped out (e.g. the lua interpretter, the web editor, etc...).

I'd like to focus on more core components of the game engine before building out the flashy stuff.

The structure of tortuga mimics that of a physical game console.

Cart

A cart (aka a game cartridge) is a representation of game code and memory.

A cart should implement the tortuga.Cart interface.

type Cart interface {
    Update()
    Render()
}

Note: that some methods can only be called in the Render function e.g. most calls that deal with drawing things.

Example

The most simple implementation of this would look like the following:

you can run this example locally with the following command

go run github.com/dfirebaugh/tortuga/examples/simple

note: click the canvas to control the rect with WASD

package main

import (
	"github.com/dfirebaugh/tortuga/pkg/input"
	"github.com/dfirebaugh/tortuga/pkg/math/geom"
	"github.com/dfirebaugh/tortuga"
)

type cart struct {
	input input.PlayerInput
}

var (
	game  tortuga.Console
	rect  geom.Rect
	speed = 4.0
)

func (c cart) Update() {
	if c.input.IsDownPressed() {
		rect[1] += speed
	}
	if c.input.IsUpPressed() {
		rect[1] -= speed
	}
	if c.input.IsLeftPressed() {
		rect[0] -= speed
	}
	if c.input.IsRightPressed() {
		rect[0] += speed
	}
}

func (c cart) Render() {
	game.Clear()

	// render a rectangle on the given display as a certain color
	// draw calls need to happen in the render loop
	rect.Draw(game.GetDisplay(), game.Color(2))
}

func main() {
	// create a rectangle when the app starts (so we don't create on every render loop)
	rect = geom.MakeRect(20, 20, 20, 20)

	// instantiate the game console
	game = tortuga.New()

	// run the cart (note: this is a blocking operation)
	game.Run(cart{
		input: input.Keyboard{},
	})
}

Geometry

The geom library provides several geometry primatives that can be drawn to the screen.

With the geom library, we can build primitives such as:

  • circles
  • rectangles
  • triangles
  • lines

Most primitives implement the Render method that accept two arguments. i.e. the display to draw to and the color that the primitive should be drawn as.

e.g.

rect := geom.MakeRect(20, 20, 20, 20)

// note the render calls can only be called inside a render loop
rect.Render(display, 2)

WAD

wad files (which stands for where's all the data represents a mapping of files that contain data used to build a scene.

wad files are written in a yaml format.

Each top level key represents a different behavior for how the asset will be marshalled into the game. File extensions don't actually matter.

example

palettes:
    main: examples/platformer/assets/main.palette
tiles:
    '#': examples/platformer/assets/brick.tile
    '$': examples/platformer/assets/block.tile
sprites:
    player: examples/platformer/assets/player.spr
    heart: examples/platformer/assets/heart.spr
backgrounds:
    platforms: examples/platformer/assets/background.map

Palette

Tortuga provides a default palette. However, each cart can define it's own palette.

A custom palette can be defined in a .palette file. .palette files should be in csv format. Each record contains 3 numbers(r, g, b) 0-255.

In tortuga, colors are referenced by their index in the palette.

Example Palette file

0,0,0,
127,36,84,
28,43,83,
0,135,81,
171,82,54,
96,88,79,
195,195,198,
255,241,233,
237,27,81,
250,162,27,
247,236,47,
93,187,77,
81,166,220,
131,118,156,
241,118,166,
252,204,171,

Tile

A tile is represented by hexadecimal digits. Each digit refers to an index on the palette.

Tile Size

Tiles are 8x8 by default. you can configure tiles to be of a different size. However, tiles can only be square.

// set tiles to be 16x16
game.SetTileSize(16)

Tile Memory

Tile memory exists as a way to store tiles in a layer that doesn't have to rerender very often.

To add a tile to tile memory:

package main

import (
	"github.com/dfirebaugh/tortuga"
	"github.com/dfirebaugh/tortuga/pkg/sprite"
)

type cart struct{}

func (c cart) Update() {}
func (c cart) Render() {}

var game tortuga.Console
func main() {
    game = tortuga.New()

    // pushes a tile to tile memory
    game.SetTile(2, 5, sprite.Parse("bbbbbbbbb4444444444455444444544445444444454444444444444444444444"))
    game.Run(cart{})
}

Creating a tile

You could dynamnically create a tile in code.

e.g.

package main

import "github.com/dfirebaugh/tortuga"

type cart struct{}

func (c cart) Update() {}
func (c cart) Render() {}

var game tortuga.Console

func blockFactory(v uint8) []uint8 {
	var b []uint8
	for i := 0; i < 8*8; i++ {
		b = append(b, v)
	}
	return b
}

func main() {
    game = tortuga.New()
    game.SetTile(2, 5, blockFactory(7))

    game.Run(cart{})
}

Example

example of a tile file:

bbbbbbbbb4444444444455444444544445444444454444444444444444444444

Map

In the wad file, you can define a representation between a character and a tile.

This provides an easy way to push a lot of tiles into tile memory.

In the wad file, we have this representation configured:

tiles:
    '#': examples/platformer/assets/brick.tile
    '$': examples/platformer/assets/block.tile
backgrounds:
    platforms: examples/platformer/assets/background.map

We can also use this file to build a collision map.

e.g.

// build a list of rectangles that somethign could collide against
func getCollidables() []geom.Rect {
	c := []geom.Rect{}
	for i, row := range strings.Split(assets.Assets.GetBackgrounds().Get("platforms"), "\n") {
		for j, char := range row {
			if char != '#' && char != '$' {
				continue
			}
			c = append(c, geom.MakeRect(
				float64(j*game.GetTileSize()),
				float64(i*game.GetTileSize()),
				float64(game.GetTileSize()),
				float64(game.GetTileSize())))
		}
	}
	return c
}

Example

$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$......................................$
$......................................$
$......................................$
$....................###...............$
$......................................$
$................###...................$
$......................................$
$............##############............$
$......................................$
$......................................$
$......################################$
$......................................$
$......................................$
$###...................................$
$......................................$
$.......#########......................$
$..................@...................$
$......................................$
$....................#######...........$
$......................................$
$......................................$
$.............................#####....$
$............######....................$
$......................................$
$....####............#######...........$
$......................................$
$......................................$
$......................................$
$######################################$

Sprite

Sprite files are similar to tile files. The main difference is that you can build out a sprite file to represent named sprite animations.

Example

idle:
    - 1111110011111111f6ff6f00ffffff000ccc00000111000001010000000000001111110011111111f6ff6f00ffffff000ccc0000011100000101000000000000
    - 3333330033333333f6ff6f00ffffff000ccc00000333000003030000000000003333330033333333f6ff6f00ffffff000ccc0000033300000303000000000000

yaml format

Textures

It's more efficient to render something to a texture and then move that texture around than it is to render directly to the frame buffer on every render call.

Textures as a display target

Textures also implement display.Displayer which means that most of tortuga's drawing libraries can draw to a texture.

Rendering Textures

Textures can be pushed into the render pipeline to be queued for rendering.

Note that textures are rendered in the order that you push them into the render pipeline.

e.g.

package main

import (
	"image/color"

	"github.com/dfirebaugh/tortuga"
	"github.com/dfirebaugh/tortuga/pkg/math/geom"
	"github.com/dfirebaugh/tortuga/pkg/texture"
)

type cart struct {
}

var (
	game tortuga.Console
)

func (c cart) Update() {}

func (c cart) Render() {
	game.FillDisplay(2)
}

var (
	circle = geom.MakeCircle(8, 8, 4)
	t      = texture.New(int(circle.Diameter()*2)+1, int(circle.Diameter()*2)+1)
)

func main() {
	game = tortuga.New()
	game.SetTransparentColor(color.Black)

	t.X = float64(game.GetScreenWidth() / 2)
	t.Y = float64(game.GetScreenHeight() / 2)
	t.Alpha = 0xFF

	circle.Filled(t, game.Color(3))

	game.AddToRenderPipeline(t)

	game.Run(cart{})
}

Simple Example

package main

type cart struct {
}

var (
	game tortuga.Console
)

func (c cart) Update() {}

func (c cart) Render() {}

var (
	heartPixels = sprite.Parse("0880088088888888888887888888888888888888088888800088880000088000")
	heartTexture = texture.New(8, 8)
)

func main() {
	game = tortuga.New()

	heartTexture.X = float64(game.GetScreenWidth() / 2)
	heartTexture.Y = float64(game.GetScreenHeight() / 2)

	heartTexture.SetPix(heartPixels)
	game.AddToRenderPipeline(heartTexture)

	game.Run(cart{})
}

Sprite Editor

You can run the sprite editor with the following command:

go run github.com/dfirebaugh/tortuga/cmd/spritely@latest

At the moment, the encoded string of what's on the canvas will be output to the console. You can also save the entire animation to a file by pressing ctrl+s.

To open an existing sprite/animation, run the following command:

go run github.com/dfirebaugh/tortuga/cmd/spritely@latest -sprite ./examples/platformer/assets/heart.spr

Map Viewer

There is a simple map viewer that can be used to preview maps.

You will have to provide a wad file and a key for which tiles should be rendered.

e.g.

go run github.com/dfirebaugh/tortuga/cmd/mapviewer@latest -wad ./cmd/mapviewer/examples/game.wad -key tiles

The wad file will need relative paths to any assets required. e.g.

tiles:
    '#': ./cmd/mapviewer/examples/brick.tile
    '$': ./cmd/mapviewer/examples/block.tile
backgrounds:
    tiles: ./cmd/mapviewer/examples/background.map

Tile Viewer

The tile viewer provides a very simple way to preview what an encoded tile looks like.

go run github.com/dfirebaugh/tortuga/cmd/tileviewer@latest -tile 0880088088888888888887888888888888888888088888800088880000088000

Text

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/font

package main

import "github.com/dfirebaugh/tortuga"

var console tortuga.Console

type cart struct{}

func (c cart) Update() {}
func (c cart) Render() {
	console.PrintAt("hello, world!", 10, 100, 4)
}

func main() {
	console = tortuga.New()
	console.SetTitle("font example")
	console.SetScaleFactor(3)
	console.Run(cart{})
}

Tiles

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/tiles

package main

import "github.com/dfirebaugh/tortuga"

type cart struct{}

func (c cart) Update() {
}

func (c cart) Render() {}

var game tortuga.Console

func blockFactory(v uint8) []uint8 {
	var b []uint8
	for i := 0; i < 8*8; i++ {
		b = append(b, v)
	}
	return b
}

func main() {
	game = tortuga.New()
	for i := 0; i < game.GetScreenWidth()/game.GetTileSize(); i++ {
		game.SetTile(i, 0, blockFactory(uint8(i+1)))
		game.SetTile(0, i, blockFactory(uint8(i+1)))
		game.SetTile(i, i, blockFactory(uint8(i+1)))
	}
	game.SetTile(2, 5, blockFactory(7))

	game.SetFPSEnabled(true)
	game.SetScaleFactor(5)
	game.Run(cart{})
}

Textures

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/texture
package main

import (
	"image/color"

	"github.com/dfirebaugh/tortuga"
	"github.com/dfirebaugh/tortuga/pkg/sprite"
	"github.com/dfirebaugh/tortuga/pkg/texture"
)

type cart struct {
}

var (
	game tortuga.Console
)

func (c cart) Update() {
}

var s = sprite.Parse("0880088088888888888887888888888888888888088888800088880000088000")
var heartTexture = texture.New(8, 8)

func (c cart) Render() {
	// setting the background to a different color
	game.FillDisplay(2)
}

func main() {
	game = tortuga.New()
	game.SetFPSEnabled(true)
	game.SetRenderPipelineDebug(true)
	game.SetTransparentColor(color.Black)

	game.SetScaleFactor(3)
	heartTexture.X = float64(game.GetScreenWidth() / 2)
	heartTexture.Y = float64(game.GetScreenHeight() / 2)

	heartTexture.SetPix(s)
	game.AddToRenderPipeline(heartTexture)

	game.SetTile(10, 20, s)
	game.SetTile(10, 10, s)

	game.RenderPalette()

	game.Run(cart{})
}

Axis Aligned Collisions

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/aabb

note: click the canvas to control the rect with WASD

package main

import (
	"github.com/dfirebaugh/tortuga/pkg/input"
	"github.com/dfirebaugh/tortuga/pkg/math/geom"
	"github.com/dfirebaugh/tortuga"
)

type cart struct {
	input input.PlayerInput
}

var (
	game  = tortuga.New()
	rect  = geom.MakeRect(float64(game.GetScreenWidth()/2-10), float64(game.GetScreenHeight()/2-10), 20, 20)
	rect1 = geom.MakeRect(float64(game.GetScreenWidth()/2-10), float64(game.GetScreenHeight()/2-10), 20, 20)
)

const (
	speed = 4
)

func (c cart) Update() {
	if c.input.IsDownPressed() {
		rect1[1] += speed
	}
	if c.input.IsUpPressed() {
		rect1[1] -= speed
	}
	if c.input.IsLeftPressed() {
		rect1[0] -= speed
	}
	if c.input.IsRightPressed() {
		rect1[0] += speed
	}
}

func (c cart) Render() {
	d := game.GetDisplay()
	game.Clear()
	rect.Draw(d, game.Color(3))

	geom.MakePoint(rect1.GetCenter()).Draw(d, uint8(12))
	if rect.IsAxisAlignedCollision(rect1) {
		rect1.Draw(d, game.Color(4))
		return
	}
	rect1.Draw(d, game.Color(2))
}

func main() {
	game.Run(cart{
		input: input.Keyboard{},
	})
}

Swept AABB

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/sweptaabb

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/sweptaabb2

Circle Collision

controls: right click generates balls, left click removes them all

Static Resolution

Static resolution ensures that the circles do not overlap.

go run github.com/dfirebaugh/tortuga/examples/staticball

Dynamic Resolution

Dynamic resolution takes into account the velocity and mass of the circle and will deflect the circle into the opposite direction of the collision.

go run github.com/dfirebaugh/tortuga/examples/dynamicball

paddleball

package main

import (
	"github.com/dfirebaugh/tortuga"
	"github.com/dfirebaugh/tortuga/pkg/component"
	"github.com/dfirebaugh/tortuga/pkg/input"
	"github.com/dfirebaugh/tortuga/pkg/math/geom"
)

type entity interface {
	Update()
	Render()
}

type cart struct {
	entities []entity
	Game     tortuga.Console
}

func (c cart) Update() {
	for _, e := range c.entities {
		e.Update()
	}
}
func (c cart) Render() {
	c.Game.Clear()
	for _, e := range c.entities {
		e.Render()
	}
}

const (
	paddleSpeed = 3
	ballSpeed   = 1.1
)

var (
	game = tortuga.New()
	ball = Ball{
		Coordinate: component.Coordinate{
			X: float64(game.GetScreenWidth()) / 2,
			Y: float64(game.GetScreenHeight()) / 2,
		},
		Velocity: component.Velocity{
			VX: ballSpeed,
			VY: ballSpeed,
		},
		Game: game,
	}
	player = paddle{
		Game:       game,
		Coordinate: component.Coordinate{X: 10, Y: 10},
		Height:     30,
		Width:      5,
		Mover: playerMover{
			Game: game,
		},
		BallPosition: &ball.Coordinate,
		Speed:        paddleSpeed,
	}
	enemy = paddle{
		Game:       game,
		Coordinate: component.Coordinate{X: float64(game.GetScreenWidth()) - 20, Y: 10},
		Height:     30,
		Width:      5,
		Mover: enemyMover{
			Game: game,
		},
		BallPosition: &ball.Coordinate,
		Speed:        paddleSpeed,
	}
)

func main() {
	game.Run(&cart{
		Game: game,
		entities: []entity{
			&ball,
			&player,
			&enemy,
		},
	})
}

type Ball struct {
	component.Coordinate
	component.Velocity
	Game tortuga.Console
}

func (b *Ball) Reset() {
	b.Coordinate.X = float64(game.GetScreenWidth()) / 2
	b.Coordinate.Y = float64(game.GetScreenHeight()) / 2

	b.VX = ballSpeed
	b.VY = ballSpeed
}

func (b *Ball) Update() {
	if b.Coordinate.Y < 0 {
		b.VY *= -ballSpeed
	}

	if b.Coordinate.Y > float64(b.Game.GetScreenHeight()) {
		b.VY *= -ballSpeed
	}

	if b.Coordinate.X > float64(b.Game.GetScreenWidth()) || b.Coordinate.X < 0 {
		b.Reset()
	}

	collisionBox := geom.MakeRect(b.X-4, b.Y-4, 4, 4)
	if collisionBox.IsAxisAlignedCollision(geom.MakeRect(enemy.X, enemy.Y, enemy.Width, enemy.Height)) ||
		collisionBox.IsAxisAlignedCollision(geom.MakeRect(player.X, player.Y, player.Width, player.Height)) {
		b.VX *= -ballSpeed
	}
	b.Coordinate.Y += b.VY
	b.Coordinate.X += b.VX
}

func (b Ball) Render() {
	geom.MakeCircle(b.X, b.Y, 1).Filled(b.Game.GetDisplay(), b.Game.Color(4))
}

type mover interface {
	Move(paddlePosition component.Coordinate, ballPosition component.Coordinate, speed float64) (x float64, y float64)
}

type playerMover struct {
	input input.Keyboard
	Game  tortuga.Console
}
type enemyMover struct {
	Game tortuga.Console
}

func (p playerMover) Move(paddlePosition component.Coordinate, ballPosition component.Coordinate, speed float64) (x float64, y float64) {
	if p.input.IsDownPressed() {
		return 0, speed
	}
	if p.input.IsUpPressed() {
		return 0, -speed
	}
	if p.input.IsLeftPressed() {
		return -speed, 0
	}
	if p.input.IsRightPressed() {
		return speed, 0
	}
	return 0, 0
}

func (e enemyMover) Move(paddlePosition component.Coordinate, ballPosition component.Coordinate, speed float64) (x float64, y float64) {
	if ballPosition.Y-10 > paddlePosition.Y {
		return 0, speed
	}

	return 0, -speed
}

type paddle struct {
	component.Coordinate
	Mover        mover
	Game         tortuga.Console
	Speed        float64
	BallPosition *component.Coordinate
	Height       float64
	Width        float64
}

func (p *paddle) Update() {
	x, y := p.Mover.Move(p.Coordinate, *p.BallPosition, p.Speed)
	p.Coordinate.X += x
	p.Coordinate.Y += y
}
func (p paddle) Render() {
	geom.MakeRect(p.X, p.Y, p.Width, p.Height).Filled(p.Game.GetDisplay(), p.Game.Color(4))
}

gameoflife

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/gameoflife
package main

import (
	"fmt"
	"math/rand"

	"github.com/dfirebaugh/tortuga"
	"github.com/dfirebaugh/tortuga/pkg/component"
	"github.com/dfirebaugh/tortuga/pkg/math/geom"
)

type cart struct {
}

var (
	game       tortuga.Console
	cells      []*cell
	generation = 0
	tick       = 0
)

func (c cart) Update() {
	if tick%5 == 0 {
		generation += 1
		for _, cell := range cells {
			cell.iteration()
		}
		for _, cell := range cells {
			cell.isAlive = cell.nextGen
		}
	}
	tick++

}
func (c cart) Render() {
	game.Clear()
	geom.MakeRect(0, 0, float64(game.GetScreenWidth()), float64(game.GetScreenHeight())).
		Filled(game.GetDisplay(), game.Color(0))
	game.PrintAt(fmt.Sprintf("gen: %d", generation), 10, 25, 2)
	for _, cell := range cells {
		cell.Render()
	}

}

type cell struct {
	geom.Rect
	isAlive bool
	component.Coordinate
	nextGen bool
}

func (c *cell) Update() {}

func (c cell) Render() {
	if !c.isAlive {
		return
	}

	c.Filled(game.GetDisplay(), game.Color(5))
}

func (c *cell) iteration() {
	if !c.isAlive {
		c.nextGen = c.shouldReproduce()
		return
	}
	c.nextGen = !c.shouldDie()
}

func (c *cell) shouldDie() bool {
	if c.isUnderpopulated() || c.isOverpopulated() {
		return true
	}

	return false
}

func (c cell) isOverpopulated() bool {
	return c.getAliveNeighborCount() > 3
}
func (c cell) isUnderpopulated() bool {
	return c.getAliveNeighborCount() < 2
}
func (c cell) shouldReproduce() bool {
	return c.getAliveNeighborCount() == 3
}

func (c cell) getAliveNeighborCount() int {
	x := c.X
	y := c.Y
	potential := []component.Coordinate{
		{Y: y, X: x - 1},     //left
		{Y: y, X: x + 1},     //right
		{Y: y - 1, X: x},     //up
		{Y: y + 1, X: x},     //down
		{Y: y - 1, X: x - 1}, //left top diagnal
		{Y: y + 1, X: x + 1}, //right bottom diagnal
		{Y: y - 1, X: x + 1}, // left bottom diagnal
		{Y: y + 1, X: x - 1}, // right top diagnal
	}

	neighbors := []bool{}

	for _, n := range potential {
		if n.X < 0 || n.Y < 0 || n.X > float64(game.GetScreenWidth()) || n.Y > float64(game.GetScreenHeight()) {
			continue
		}

		if game.GetScreenWidth()*int(n.Y)+int(n.X) >= len(cells) {
			continue
		}
		if cells[game.GetScreenWidth()*int(n.Y)+int(n.X)].isAlive {
			neighbors = append(neighbors, true)
		}
	}

	return len(neighbors)
}

func main() {
	game = tortuga.New()
	game.SetScaleFactor(3)
	game.SetFPSEnabled(true)

	for y := 0; y < game.GetScreenHeight(); y++ {
		for x := 0; x < game.GetScreenWidth(); x++ {
			c := &cell{}
			c.X = float64(x)
			c.Y = float64(y)
			c.Rect = geom.MakeRect(c.X, c.Y, 1, 1)
			c.isAlive = rand.Intn(100) < 25
			cells = append(cells, c)
		}
	}

	game.Run(cart{})
}

sfx

note: audio is a work in progress.

tone

note: audio is a work in progress.


package main

import (
	"time"

	"github.com/dfirebaugh/tortuga"
	"github.com/dfirebaugh/tortuga/pkg/input"
)

type cart struct {
	input       input.PlayerInput
	game        tortuga.Console
	notes       []string
	currentNote int
}

func (c *cart) Update() {
	if c.input.IsUpJustPressed() {
		if c.currentNote == 0 {
			c.currentNote = len(c.notes)
		}
		c.currentNote = (c.currentNote - 1) % len(c.notes)
	}
	if c.input.IsDownJustPressed() {
		c.currentNote = (c.currentNote + 1) % len(c.notes)
	}
	if c.input.IsRightJustPressed() {
		c.game.PlayNote(c.game.Notes()[c.notes[c.currentNote]], time.Millisecond*250)
	}
	if c.input.IsLeftJustPressed() {
		go func() {
			c.game.PlayNote(c.game.Frequency("c4"), time.Second)
		}()
		go func() {
			c.game.PlayNote(c.game.Frequency("e4"), time.Second)
		}()
		go func() {
			c.game.PlayNote(c.game.Frequency("c4"), time.Second)
		}()
	}
}

func (c *cart) Render() {
	c.game.Clear()
	c.game.PrintAt(c.notes[c.currentNote], c.game.GetScreenWidth()/2, c.game.GetScreenHeight()/2, 6)
	c.game.PrintAt("press right arrow to play tone", 10, 180, 5)
	c.game.PrintAt("press up/down arrow to select a different note", 10, 195, 5)
}

func main() {
	game := tortuga.New()
	notes := []string{}
	for n := range game.Notes() {
		notes = append(notes, n)
	}

	game.Run(&cart{
		input: input.Keyboard{},
		game:  game,
		notes: notes,
	})
}

sequence

note: audio is a work in progress.


package main

import (
	"time"

	"github.com/dfirebaugh/tortuga"
	"github.com/dfirebaugh/tortuga/pkg/input"
)

type cart struct {
	input input.PlayerInput
	game  tortuga.Console
}

var (
	happy = []float32{
		264,
		264,
		297,
		264,
		352,
		330,
		264,
		264,
		297,
		264,
		396,
		352,
		264,
		264,
		264,
		440,
		352,
		352,
		330,
		297,
		466,
		466,
		440,
		352,
		396,
		352,
	}

	ode = []string{
		"e4",
		"e4",
		"f4",
		"g4",
		"g4",
		"f4",
		"e4",
		"d4",
		"c4",
		"c4",
		"d4",
		"e4",
		"e4",
		"d4",
		"d4",
		"e4",
		"e4",
		"f4",
		"g4",
		"g4",
		"f4",
		"e4",
		"d4",
		"c4",
		"c4",
		"d4",
		"e4",
		"d4",
		"c4",
		"c4",
	}
)

func (c cart) Update() {
	if c.input.IsDownJustPressed() {
		go func() {
			c.game.PlayNotes(ode, time.Millisecond*550)
		}()
	}
	if c.input.IsUpJustPressed() {
		go func() {
			c.game.PlaySequence(happy, time.Millisecond*250)
		}()
	}
}

func (c cart) Render() {
	c.game.PrintAt("press up arrow to play sequence 1", 10, 170, 5)
	c.game.PrintAt("press down arrow to play sequence 2", 10, 180, 5)
}

func main() {
	game := tortuga.New()
	game.Run(cart{
		input: input.Keyboard{},
		game:  game,
	})
}

spectrum

you can run this locally with the following command

go run github.com/dfirebaugh/tortuga/examples/spectrum