aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfeurig <don@suspectdevices.com>2021-12-05 18:29:18 -0800
committerfeurig <don@suspectdevices.com>2021-12-05 18:29:18 -0800
commitcf8db4d4057cbdeeed6bdcb2aa35f19cea4d09f7 (patch)
tree5e751ea0099da923ea930db19cb2bcb3ec453818
Steal this framework
-rw-r--r--.gitignore21
-rw-r--r--LICENSE21
-rw-r--r--README.md19
-rw-r--r--go.mod8
-rw-r--r--go.sum6
-rw-r--r--images/game_over_screen.pngbin0 -> 9816 bytes
-rw-r--r--images/running_game_screen.pngbin0 -> 6421 bytes
-rw-r--r--images/title_screen.pngbin0 -> 9410 bytes
-rw-r--r--invaders/alien.go92
-rw-r--r--invaders/alien_cluster.go214
-rw-r--r--invaders/arena.go71
-rw-r--r--invaders/entity.go10
-rw-r--r--invaders/files/alien_basic.txt2
-rw-r--r--invaders/files/alien_medium.txt2
-rw-r--r--invaders/files/alien_strong.txt2
-rw-r--r--invaders/files/game_over.txt6
-rw-r--r--invaders/files/hero.txt2
-rw-r--r--invaders/files/title.txt6
-rw-r--r--invaders/game.go217
-rw-r--r--invaders/game_over_zone.go27
-rw-r--r--invaders/hero.go104
-rw-r--r--invaders/hud.go61
-rw-r--r--invaders/laser.go46
-rw-r--r--invaders/screens.go97
-rw-r--r--invaders/utils/utils.go5
-rw-r--r--main.go7
26 files changed, 1046 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..996f1ae
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5afc313
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Archimedes Fagundes Junior
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8908dda
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# Go Invaders
+A Space Invaders inspired game written in Go. Built with [Termloop](https://github.com/JoelOtter/termloop).
+
+## Installation
+```
+go install github.com/afagundes/go-invaders@latest
+$GOPATH/bin/go-invaders
+```
+
+## Controls
+Use the arrow keys to move the hero spaceship and space bar to shoot. Press backspace to quit.
+
+# Screenshots
+
+![Image of title screen](https://github.com/afagundes/go-invaders/blob/main/images/title_screen.png)
+
+![Image of game screen](https://github.com/afagundes/go-invaders/blob/main/images/running_game_screen.png)
+
+![Image of game over screen](https://github.com/afagundes/go-invaders/blob/main/images/game_over_screen.png)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a4356d2
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,8 @@
+module github.com/afagundes/go-invaders
+
+go 1.16
+
+require (
+ github.com/JoelOtter/termloop v0.0.0-20201118115657-7fa23b4da654
+ github.com/nsf/termbox-go v1.1.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..cb40fd6
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,6 @@
+github.com/JoelOtter/termloop v0.0.0-20201118115657-7fa23b4da654 h1:pQt338SARhakABQ3KbycsnuKSg3XRDA5sF5RHl/v9WQ=
+github.com/JoelOtter/termloop v0.0.0-20201118115657-7fa23b4da654/go.mod h1:Tie7OOEgasw91JpzA8UywemPyGehxZ06Gqtl5B1/vXI=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
+github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
diff --git a/images/game_over_screen.png b/images/game_over_screen.png
new file mode 100644
index 0000000..9fd2dd0
--- /dev/null
+++ b/images/game_over_screen.png
Binary files differ
diff --git a/images/running_game_screen.png b/images/running_game_screen.png
new file mode 100644
index 0000000..cabd95d
--- /dev/null
+++ b/images/running_game_screen.png
Binary files differ
diff --git a/images/title_screen.png b/images/title_screen.png
new file mode 100644
index 0000000..1b28f0b
--- /dev/null
+++ b/images/title_screen.png
Binary files differ
diff --git a/invaders/alien.go b/invaders/alien.go
new file mode 100644
index 0000000..f73b7e1
--- /dev/null
+++ b/invaders/alien.go
@@ -0,0 +1,92 @@
+package invaders
+
+import (
+ _ "embed"
+ tl "github.com/JoelOtter/termloop"
+)
+
+type Alien struct {
+ *tl.Entity
+ IsAlive bool
+ IsRendered bool
+ Points int
+}
+
+type AlienType struct {
+ Source []byte
+ Points int
+}
+
+var (
+ //go:embed files/alien_basic.txt
+ alienBasicBytes []byte
+
+ //go:embed files/alien_medium.txt
+ alienMediumBytes []byte
+
+ //go:embed files/alien_strong.txt
+ alienStrongBytes []byte
+
+ Basic = AlienType{Source: alienBasicBytes, Points: 10}
+ Medium = AlienType{Source: alienMediumBytes, Points: 20}
+ Strong = AlienType{Source: alienStrongBytes, Points: 30}
+)
+
+func NewAlien(alienType AlienType) *Alien {
+ canvas := CreateCanvas(alienType.Source)
+ return &Alien{Entity: tl.NewEntityFromCanvas(0, 0, canvas), IsAlive: true, Points: alienType.Points}
+}
+
+func CreateAliensLine(alienType AlienType, lineSize int) []*Alien {
+ aliens := make([]*Alien, lineSize)
+ for i := 0; i < lineSize; i++ {
+ aliens[i] = NewAlien(alienType)
+ }
+
+ return aliens
+}
+
+func SetPositionAndRenderAliens(aliens [][]*Alien, level *tl.BaseLevel, arena *Arena) {
+ initialX, initialY, space := calcInitialPositionAndSpace(aliens, arena)
+
+ for index, line := range aliens {
+ x := initialX
+
+ for _, alien := range line {
+ _, height := alien.Size()
+ y := initialY + height*(index+1) - 2
+
+ alien.SetPosition(x, y)
+ alien.IsRendered = true
+
+ level.AddEntity(alien)
+
+ x += space
+ }
+ }
+}
+
+func calcInitialPositionAndSpace(aliens [][]*Alien, arena *Arena) (int, int, int) {
+ lineSize := len(aliens[0])
+ alienW, _ := aliens[0][0].Size()
+ space := alienW + 1
+
+ arenaX, arenaY := arena.Position()
+ arenaW, _ := arena.Size()
+
+ totalWidth := lineSize * space
+ x := arenaX + arenaW/2 - totalWidth/2
+
+ return x, arenaY, space
+}
+
+func (alien *Alien) Collide(collision tl.Physical) {
+ if _, ok := collision.(*Laser); ok {
+ laser := collision.(*Laser)
+
+ if laser.IsFromHero {
+ laser.HasHit = true
+ alien.IsAlive = false
+ }
+ }
+}
diff --git a/invaders/alien_cluster.go b/invaders/alien_cluster.go
new file mode 100644
index 0000000..62bcdcf
--- /dev/null
+++ b/invaders/alien_cluster.go
@@ -0,0 +1,214 @@
+package invaders
+
+import (
+ tl "github.com/JoelOtter/termloop"
+ "math/rand"
+ "time"
+)
+
+type AlienCluster struct {
+ Aliens [][]*Alien
+ Lasers []*Laser
+ TimeToMove float64
+ WaitingTime float64
+ WaitingTimeToMoveNextRow float64
+ CurrentRowMoving int
+ Direction int
+ MoveSize int
+ IsMoving bool
+ IsMovingDown bool
+ IsAllDead bool
+ ReachedEndArena bool
+}
+
+func NewAlienCluster() *AlienCluster {
+ lineSize := 13
+
+ alienCluster := AlienCluster{
+ TimeToMove: 0,
+ WaitingTime: 1,
+ WaitingTimeToMoveNextRow: 0.07,
+ CurrentRowMoving: -1,
+ Direction: 3,
+ MoveSize: 3,
+ IsMoving: false,
+ ReachedEndArena: false,
+ IsMovingDown: false,
+ }
+
+ alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Strong, lineSize))
+ alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Medium, lineSize))
+ alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Medium, lineSize))
+ alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Basic, lineSize))
+ alienCluster.Aliens = append(alienCluster.Aliens, CreateAliensLine(Basic, lineSize))
+
+ return &alienCluster
+}
+
+func (alienCluster *AlienCluster) UpdateAliensPositions(timeDelta float64, arena *Arena) {
+ alienCluster.prepareMovement()
+
+ if alienCluster.canMove(timeDelta) {
+ alienCluster.move(arena)
+ }
+}
+
+func (alienCluster *AlienCluster) prepareMovement() {
+ if alienCluster.CurrentRowMoving == -1 {
+ alienCluster.CurrentRowMoving = len(alienCluster.Aliens) - 1
+ alienCluster.IsMoving = false
+ }
+
+ if alienCluster.ReachedEndArena {
+ alienCluster.changeDirection()
+ }
+}
+
+func (alienCluster *AlienCluster) changeDirection() {
+ alienCluster.Direction *= -1
+ alienCluster.ReachedEndArena = false
+}
+
+func (alienCluster *AlienCluster) canMove(timeDelta float64) bool {
+ alienCluster.TimeToMove += timeDelta
+ if alienCluster.isWaitingToMoveFirstRow() || alienCluster.isWaitingToMoveNextRow() {
+ return false
+ }
+ return true
+}
+
+func (alienCluster *AlienCluster) isWaitingToMoveFirstRow() bool {
+ return alienCluster.IsMoving == false && alienCluster.TimeToMove < alienCluster.WaitingTime
+}
+
+func (alienCluster *AlienCluster) isWaitingToMoveNextRow() bool {
+ return alienCluster.IsMoving && alienCluster.TimeToMove < alienCluster.WaitingTimeToMoveNextRow
+}
+
+func (alienCluster *AlienCluster) move(arena *Arena) {
+ alienCluster.TimeToMove = 0
+ alienCluster.IsMoving = true
+
+ if alienCluster.IsMovingDown {
+ alienCluster.moveDown()
+ } else {
+ alienCluster.moveSideways(arena)
+ }
+
+ alienCluster.CurrentRowMoving -= 1
+}
+
+func (alienCluster *AlienCluster) moveDown() {
+ row := alienCluster.getCurrentLine()
+
+ for _, alien := range row {
+ x, y := alien.Position()
+ alien.SetPosition(x, y+alienCluster.MoveSize)
+ }
+
+ finishedMovingRows := alienCluster.CurrentRowMoving == 0
+ if finishedMovingRows {
+ alienCluster.IsMovingDown = false
+ }
+}
+
+func (alienCluster *AlienCluster) moveSideways(arena *Arena) {
+ row := alienCluster.getCurrentLine()
+
+ for _, alien := range row {
+ x, y := alien.Position()
+ w, _ := alien.Size()
+ x = x + alienCluster.Direction
+
+ alien.SetPosition(x, y)
+
+ if alien.IsAlive {
+ alienCluster.checkIfReachedEndOfArena(x, w, arena)
+ }
+ }
+}
+
+func (alienCluster *AlienCluster) getCurrentLine() []*Alien {
+ row := alienCluster.Aliens[alienCluster.CurrentRowMoving]
+ return row
+}
+
+func (alienCluster *AlienCluster) checkIfReachedEndOfArena(x int, w int, arena *Arena) {
+ if alienCluster.CurrentRowMoving == 0 && (x+w >= arena.End-alienCluster.MoveSize || x <= arena.Init+alienCluster.MoveSize) {
+ alienCluster.ReachedEndArena = true
+ alienCluster.IsMovingDown = true
+ }
+}
+
+func (alienCluster *AlienCluster) RemoveDeadAliensAndGetPoints(level *tl.BaseLevel) int {
+ hasAtLeastOneAlive := false
+ points := 0
+
+ for _, alienRow := range alienCluster.Aliens {
+ for _, alien := range alienRow {
+ if alien.IsAlive == false && alien.IsRendered == true {
+ points += alien.Points
+ alien.IsRendered = false
+
+ level.RemoveEntity(alien)
+ }
+
+ if alien.IsAlive == true {
+ hasAtLeastOneAlive = true
+ }
+ }
+ }
+
+ alienCluster.IsAllDead = !hasAtLeastOneAlive
+
+ return points
+}
+
+func (alienCluster *AlienCluster) Shoot() {
+ if alienCluster.canShoot() {
+ alien := alienCluster.selectRandomAlien()
+
+ if alien == nil {
+ return
+ }
+
+ x, y := alien.Position()
+ width, _ := alien.Size()
+ alienGunPosition := x + (width-1)/2
+ distanceToAlien := y + 2
+
+ laser := NewAlienLaser(alienGunPosition, distanceToAlien)
+ alienCluster.Lasers = append(alienCluster.Lasers, laser)
+ }
+}
+
+func (alienCluster *AlienCluster) canShoot() bool {
+ return len(alienCluster.Lasers) == 0
+}
+
+func (alienCluster *AlienCluster) selectRandomAlien() *Alien {
+ var shooterAlien *Alien
+
+ rowSize := len(alienCluster.Aliens) - 1
+ col := alienCluster.selectRandomAlienColumn()
+
+ for row := rowSize; row >= 0; row-- {
+ alien := alienCluster.Aliens[row][col]
+
+ if alien.IsAlive {
+ shooterAlien = alien
+ break
+ }
+ }
+ return shooterAlien
+}
+
+func (alienCluster *AlienCluster) selectRandomAlienColumn() int {
+ rand.Seed(time.Now().UnixNano())
+ col := rand.Intn(len(alienCluster.Aliens[0]))
+ return col
+}
+
+func (alienCluster *AlienCluster) IsAllAliensDead() bool {
+ return alienCluster.IsAllDead
+}
diff --git a/invaders/arena.go b/invaders/arena.go
new file mode 100644
index 0000000..440db57
--- /dev/null
+++ b/invaders/arena.go
@@ -0,0 +1,71 @@
+package invaders
+
+import (
+ tl "github.com/JoelOtter/termloop"
+ "github.com/afagundes/go-invaders/invaders/utils"
+)
+
+type Arena struct {
+ *tl.Entity
+ Init int
+ End int
+}
+
+const (
+ ArenaMaxWidth = 100
+ ArenaMaxHeight = 37
+)
+
+func newArena(screenWidth int, screenHeight int) *Arena {
+ width := utils.ValueMinusPercent(screenWidth, 0.40)
+ height := utils.ValueMinusPercent(screenHeight, 0.15)
+
+ if width > ArenaMaxWidth {
+ width = ArenaMaxWidth
+ }
+
+ if height > ArenaMaxHeight {
+ height = ArenaMaxHeight
+ }
+
+ centerX := screenWidth/2 - width/2
+ centerY := screenHeight/2 - height/2
+ init := centerX + 1
+ end := centerX + width
+
+ return &Arena{tl.NewEntityFromCanvas(centerX, centerY, createArena(width, height)), init, end}
+}
+
+func createArena(width, height int) tl.Canvas {
+ canvas := tl.NewCanvas(width, height)
+
+ for x, cell := range canvas {
+ for y := range cell {
+ fillTopBottom(x, y, height, canvas)
+ fillSides(x, y, width, canvas)
+ }
+ }
+
+ createCell(0, 0, canvas, '┌')
+ createCell(width-1, 0, canvas, '┐')
+ createCell(0, height-1, canvas, '└')
+ createCell(width-1, height-1, canvas, '┘')
+
+ return canvas
+}
+
+func fillTopBottom(x, y, height int, canvas tl.Canvas) {
+ if x > 0 && (y == 0 || y == height-1) {
+ createCell(x, y, canvas, '─')
+ }
+}
+
+func fillSides(x, y, width int, canvas tl.Canvas) {
+ if x == 0 || x == width-1 {
+ createCell(x, y, canvas, '│')
+ }
+}
+
+func createCell(x, y int, canvas tl.Canvas, ch rune) {
+ canvas[x][y] = tl.Cell{Bg: tl.ColorBlack, Fg: tl.ColorWhite, Ch: ch}
+}
diff --git a/invaders/entity.go b/invaders/entity.go
new file mode 100644
index 0000000..023aace
--- /dev/null
+++ b/invaders/entity.go
@@ -0,0 +1,10 @@
+package invaders
+
+import (
+ tl "github.com/JoelOtter/termloop"
+)
+
+func CreateCanvas(fileContent []byte) tl.Canvas {
+ canvas := tl.CanvasFromString(string(fileContent))
+ return canvas
+}
diff --git a/invaders/files/alien_basic.txt b/invaders/files/alien_basic.txt
new file mode 100644
index 0000000..ca67a36
--- /dev/null
+++ b/invaders/files/alien_basic.txt
@@ -0,0 +1,2 @@
+▄██▄
+
diff --git a/invaders/files/alien_medium.txt b/invaders/files/alien_medium.txt
new file mode 100644
index 0000000..1e19062
--- /dev/null
+++ b/invaders/files/alien_medium.txt
@@ -0,0 +1,2 @@
+▄██▄
+▀ ▀
diff --git a/invaders/files/alien_strong.txt b/invaders/files/alien_strong.txt
new file mode 100644
index 0000000..25853d0
--- /dev/null
+++ b/invaders/files/alien_strong.txt
@@ -0,0 +1,2 @@
+█ █
+▀██▀
diff --git a/invaders/files/game_over.txt b/invaders/files/game_over.txt
new file mode 100644
index 0000000..971d929
--- /dev/null
+++ b/invaders/files/game_over.txt
@@ -0,0 +1,6 @@
+ ██████╗ █████╗ ███╗ ███╗███████╗ ██████╗ ██╗ ██╗███████╗██████╗
+██╔════╝ ██╔══██╗████╗ ████║██╔════╝ ██╔═══██╗██║ ██║██╔════╝██╔══██╗
+██║ ███╗███████║██╔████╔██║█████╗ ██║ ██║██║ ██║█████╗ ██████╔╝
+██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗
+╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗ ╚██████╔╝ ╚████╔╝ ███████╗██║ ██║
+ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
diff --git a/invaders/files/hero.txt b/invaders/files/hero.txt
new file mode 100644
index 0000000..1edcd05
--- /dev/null
+++ b/invaders/files/hero.txt
@@ -0,0 +1,2 @@
+ ▄
+███
diff --git a/invaders/files/title.txt b/invaders/files/title.txt
new file mode 100644
index 0000000..fba236e
--- /dev/null
+++ b/invaders/files/title.txt
@@ -0,0 +1,6 @@
+ ██████╗ ██████╗ ██╗███╗ ██╗██╗ ██╗ █████╗ ██████╗ ███████╗██████╗ ███████╗
+██╔════╝ ██╔═══██╗ ██║████╗ ██║██║ ██║██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝
+██║ ███╗██║ ██║ ██║██╔██╗ ██║██║ ██║███████║██║ ██║█████╗ ██████╔╝███████╗
+██║ ██║██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══██║██║ ██║██╔══╝ ██╔══██╗╚════██║
+╚██████╔╝╚██████╔╝ ██║██║ ╚████║ ╚████╔╝ ██║ ██║██████╔╝███████╗██║ ██║███████║
+ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═══╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚══════╝
diff --git a/invaders/game.go b/invaders/game.go
new file mode 100644
index 0000000..54052de
--- /dev/null
+++ b/invaders/game.go
@@ -0,0 +1,217 @@
+package invaders
+
+import (
+ tl "github.com/JoelOtter/termloop"
+ "time"
+)
+
+type Invaders struct {
+ *tl.Entity
+ Game *tl.Game
+ Level *tl.BaseLevel
+ Arena *Arena
+ GameOverZone *GameOverZone
+ Hud *Hud
+ Hero *Hero
+ AlienCluster *AlienCluster
+ AlienLaserVelocity float64
+ TimeDelta float64
+ RefreshSpeed time.Duration
+ Score int
+ Started bool
+ ScreenSizeNotOK bool
+}
+
+func NewGame() *Invaders {
+ invaders := Invaders{
+ Entity: tl.NewEntity(0, 0, 1, 1),
+ Game: tl.NewGame(),
+ Level: tl.NewBaseLevel(tl.Cell{Bg: tl.ColorBlack, Fg: tl.ColorWhite}),
+ AlienLaserVelocity: 0.04,
+ RefreshSpeed: 20,
+ Score: 0,
+ }
+
+ invaders.Game.Screen().SetFps(60)
+ invaders.Game.SetEndKey(tl.KeyBackspace)
+ invaders.Game.Screen().SetLevel(invaders.Level)
+ invaders.Level.AddEntity(&invaders)
+
+ return &invaders
+}
+
+func (invaders *Invaders) Start() {
+ go ShowTitleScreen(invaders)
+ invaders.Game.Start()
+}
+
+func (invaders *Invaders) Tick(event tl.Event) {
+ if invaders.Started == false && invaders.ScreenSizeNotOK == false && event.Type == tl.EventKey && event.Key == tl.KeyEnter {
+ go invaders.initializeGame()
+ }
+}
+
+func (invaders *Invaders) initializeGame() {
+ prepareScreen(invaders)
+
+ invaders.Started = true
+
+ invaders.initHero()
+ invaders.initAliens()
+ invaders.initGameOverZone()
+ invaders.gameLoop()
+}
+
+func (invaders *Invaders) initArena() {
+ screenWidth, screenHeight := invaders.getScreenSize()
+ invaders.Arena = newArena(screenWidth, screenHeight)
+ invaders.Level.AddEntity(invaders.Arena)
+}
+
+func (invaders *Invaders) initHud() {
+ invaders.Hud = NewHud(invaders.Arena, invaders.Level)
+}
+
+func (invaders *Invaders) getScreenSize() (int, int) {
+ screenWidth, screenHeight := invaders.Game.Screen().Size()
+
+ for screenWidth == 0 && screenHeight == 0 {
+ time.Sleep(100 * time.Millisecond)
+ screenWidth, screenHeight = invaders.Game.Screen().Size()
+ }
+
+ return screenWidth, screenHeight
+}
+
+func (invaders *Invaders) initHero() {
+ invaders.Hero = NewHero(invaders.Arena)
+ invaders.Level.AddEntity(invaders.Hero)
+}
+
+func (invaders *Invaders) initAliens() {
+ invaders.AlienCluster = NewAlienCluster()
+ SetPositionAndRenderAliens(invaders.AlienCluster.Aliens, invaders.Level, invaders.Arena)
+}
+
+func (invaders *Invaders) initGameOverZone() {
+ invaders.GameOverZone = CreateGameOverZone(invaders.Arena, invaders.Hero)
+ invaders.Level.AddEntity(invaders.GameOverZone)
+}
+
+func (invaders *Invaders) gameLoop() {
+ for {
+ if invaders.Hero.IsDead() || invaders.AlienCluster.IsAllAliensDead() {
+ invaders.Hero.animateHeroEndGame(invaders.Level)
+ invaders.Started = false
+ break
+ }
+
+ invaders.updateLaserPositions()
+ invaders.RemoveDeadAliensAndIncrementScore()
+ invaders.updateAlienClusterPosition()
+ invaders.updateScore()
+ invaders.verifyGameOverZone()
+
+ time.Sleep(invaders.RefreshSpeed * time.Millisecond)
+ }
+
+ if invaders.Hero.IsDead() {
+ ShowGameOverScreen(invaders)
+ }
+
+ if invaders.AlienCluster.IsAllAliensDead() {
+ invaders.initializeGame()
+ }
+}
+
+func (invaders *Invaders) updateScore() {
+ invaders.Hud.UpdateScore(invaders.Score)
+}
+
+func (invaders *Invaders) updateAlienClusterPosition() {
+ invaders.AlienCluster.UpdateAliensPositions(invaders.Game.Screen().TimeDelta(), invaders.Arena)
+ invaders.AlienCluster.Shoot()
+}
+
+func (invaders *Invaders) RemoveDeadAliensAndIncrementScore() {
+ points := invaders.AlienCluster.RemoveDeadAliensAndGetPoints(invaders.Level)
+ invaders.addScore(points)
+}
+
+func (invaders *Invaders) updateLaserPositions() {
+ invaders.updateHeroLasers()
+ invaders.updateAlienLasers()
+ invaders.removeLasers()
+}
+
+func (invaders *Invaders) updateHeroLasers() {
+ invaders.updateLasers(invaders.Hero.Lasers)
+}
+
+func (invaders *Invaders) updateAlienLasers() {
+ invaders.TimeDelta += invaders.Game.Screen().TimeDelta()
+
+ if invaders.TimeDelta >= invaders.AlienLaserVelocity {
+ invaders.TimeDelta = 0
+ invaders.updateLasers(invaders.AlienCluster.Lasers)
+ }
+}
+
+func (invaders *Invaders) updateLasers(lasers []*Laser) {
+ for _, laser := range lasers {
+ if laser.IsNew {
+ invaders.renderNewLaser(laser)
+ continue
+ }
+
+ x, y := laser.Position()
+ laser.SetPosition(x, y-laser.Direction)
+ }
+}
+
+func (invaders *Invaders) renderNewLaser(laser *Laser) {
+ laser.IsNew = false
+ invaders.Level.AddEntity(laser)
+}
+
+func (invaders *Invaders) removeLasers() {
+ _, arenaY := invaders.Arena.Position()
+ _, arenaH := invaders.Arena.Size()
+
+ upperLimit := arenaY
+ bottomLimit := arenaY + arenaH - 1
+
+ invaders.Hero.Lasers = invaders.removeLaserOf(invaders.Hero.Lasers, upperLimit)
+ invaders.AlienCluster.Lasers = invaders.removeLaserOf(invaders.AlienCluster.Lasers, bottomLimit)
+}
+
+func (invaders *Invaders) removeLaserOf(lasers []*Laser, arenaLimit int) []*Laser {
+ for index, laser := range lasers {
+ _, y := laser.Position()
+ isEndOfArena := y == arenaLimit
+
+ if isEndOfArena || laser.HasHit {
+ invaders.Level.RemoveEntity(laser)
+
+ if laser.HitAlienLaser {
+ invaders.addScore(laser.Points)
+ }
+
+ if index < len(lasers) {
+ lasers = append(lasers[:index], lasers[index+1:]...)
+ }
+ }
+ }
+
+ return lasers
+}
+
+func (invaders *Invaders) addScore(points int) {
+ invaders.Score += points
+}
+
+func (invaders *Invaders) verifyGameOverZone() {
+ if invaders.GameOverZone.EnteredZone {
+ invaders.Hero.IsAlive = false
+ }
+}
diff --git a/invaders/game_over_zone.go b/invaders/game_over_zone.go
new file mode 100644
index 0000000..702deb4
--- /dev/null
+++ b/invaders/game_over_zone.go
@@ -0,0 +1,27 @@
+package invaders
+
+import tl "github.com/JoelOtter/termloop"
+
+type GameOverZone struct {
+ *tl.Entity
+ EnteredZone bool
+}
+
+func CreateGameOverZone(arena *Arena, hero *Hero) *GameOverZone {
+ _, heroH := hero.Size()
+ arenaX, arenaY := arena.Position()
+ arenaW, arenaH := arena.Size()
+
+ x := arenaX
+ y := arenaY + arenaH - heroH
+ w := arenaW
+ h := heroH
+
+ return &GameOverZone{Entity: tl.NewEntity(x, y, w, h)}
+}
+
+func (gameOverZone *GameOverZone) Collide(collision tl.Physical) {
+ if _, ok := collision.(*Alien); ok && collision.(*Alien).IsAlive {
+ gameOverZone.EnteredZone = true
+ }
+}
diff --git a/invaders/hero.go b/invaders/hero.go
new file mode 100644
index 0000000..c9b4500
--- /dev/null
+++ b/invaders/hero.go
@@ -0,0 +1,104 @@
+package invaders
+
+import (
+ _ "embed"
+ tl "github.com/JoelOtter/termloop"
+ "time"
+)
+
+type Hero struct {
+ *tl.Entity
+ Arena *Arena
+ Lasers []*Laser
+ IsAlive bool
+}
+
+//go:embed files/hero.txt
+var heroBytes []byte
+
+func NewHero(arena *Arena) *Hero {
+ heroCanvas := CreateCanvas(heroBytes)
+ x, y := setHeroPosition(arena, heroCanvas)
+
+ return &Hero{Entity: tl.NewEntityFromCanvas(x, y, heroCanvas), Arena: arena, IsAlive: true}
+}
+
+func setHeroPosition(arena *Arena, heroCanvas tl.Canvas) (int, int) {
+ arenaX, arenaY := arena.Position()
+ arenaW, arenaH := arena.Size()
+
+ x := arenaX + arenaW/2 - len(heroCanvas)/2
+ y := arenaY + arenaH - len(heroCanvas[0])
+
+ return x, y
+}
+
+func (hero *Hero) Tick(event tl.Event) {
+ if hero.IsAlive == false {
+ return
+ }
+
+ if event.Type == tl.EventKey {
+ x, y := hero.Position()
+ heroWidth, _ := hero.Size()
+
+ switch event.Key {
+ case tl.KeyArrowLeft:
+ if x > hero.Arena.Init {
+ x = x - 1
+ hero.SetPosition(x, y)
+ }
+ case tl.KeyArrowRight:
+ if x < hero.Arena.End-heroWidth-1 {
+ x = x + 1
+ hero.SetPosition(x, y)
+ }
+ case tl.KeySpace:
+ hero.shoot()
+ }
+ }
+}
+
+func (hero *Hero) shoot() {
+ x, y := hero.Position()
+
+ if hero.isReloading() {
+ return
+ }
+
+ heroWidth, _ := hero.Size()
+ heroGunPosition := x + (heroWidth-1)/2
+ distanceToHero := y - 1
+
+ laser := NewHeroLaser(heroGunPosition, distanceToHero)
+ hero.Lasers = append(hero.Lasers, laser)
+}
+
+func (hero *Hero) isReloading() bool {
+ return len(hero.Lasers) > 0
+}
+
+func (hero *Hero) Collide(collision tl.Physical) {
+ if _, ok := collision.(*Laser); ok {
+ laser := collision.(*Laser)
+
+ laser.HasHit = true
+ hero.IsAlive = false
+ }
+}
+
+func (hero *Hero) IsDead() bool {
+ return hero.IsAlive == false
+}
+
+func (hero *Hero) animateHeroEndGame(level *tl.BaseLevel) {
+ for i := 0; i < 6; i++ {
+ if i%2 == 0 {
+ level.RemoveEntity(hero)
+ } else {
+ level.AddEntity(hero)
+ }
+
+ time.Sleep(450 * time.Millisecond)
+ }
+}
diff --git a/invaders/hud.go b/invaders/hud.go
new file mode 100644
index 0000000..cbe2c8e
--- /dev/null
+++ b/invaders/hud.go
@@ -0,0 +1,61 @@
+package invaders
+
+import (
+ "fmt"
+ tl "github.com/JoelOtter/termloop"
+)
+
+type Hud struct {
+ Title string
+ PressToQuit string
+ Score *tl.FpsText
+}
+
+func NewHud(arena *Arena, level *tl.BaseLevel) *Hud {
+ hud := Hud{Title: "Go Invaders", PressToQuit: "Press BACKSPACE to quit"}
+ hud.drawTitle(arena, level)
+ hud.drawPressToQuit(arena, level)
+ hud.drawScore(arena, level)
+
+ return &hud
+}
+
+func (hud *Hud) drawTitle(arena *Arena, level *tl.BaseLevel) {
+ arenaX, arenaY := arena.Position()
+ x := arenaX + 1
+ y := arenaY - 1
+
+ title := tl.NewText(x, y, hud.Title, tl.ColorWhite, tl.ColorBlack)
+ level.AddEntity(title)
+}
+
+func (hud *Hud) drawPressToQuit(arena *Arena, level *tl.BaseLevel) {
+ arenaX, arenaY := arena.Position()
+ arenaW, arenaH := arena.Size()
+
+ x := arenaX + arenaW - len(hud.PressToQuit) - 1
+ y := arenaY + arenaH
+
+ title := tl.NewText(x, y, hud.PressToQuit, tl.ColorWhite, tl.ColorBlack)
+ level.AddEntity(title)
+}
+
+func (hud *Hud) drawScore(arena *Arena, level *tl.BaseLevel) {
+ arenaX, arenaY := arena.Position()
+ _, arenaH := arena.Size()
+
+ x := arenaX + 1
+ y := arenaY + arenaH
+
+ hud.Score = tl.NewFpsText(x, y, tl.ColorWhite, tl.ColorBlack, 60)
+ level.AddEntity(hud.Score)
+}
+
+func (hud *Hud) UpdateScore(score int) {
+ hud.Score.SetText(hud.getScoreText(score))
+}
+
+func (hud *Hud) getScoreText(score int) string {
+ txtScore := fmt.Sprintf("Score: %4d", score)
+ return txtScore
+}
diff --git a/invaders/laser.go b/invaders/laser.go
new file mode 100644
index 0000000..d2d532d
--- /dev/null
+++ b/invaders/laser.go
@@ -0,0 +1,46 @@
+package invaders
+
+import tl "github.com/JoelOtter/termloop"
+
+type Laser struct {
+ *tl.Rectangle
+ Direction int
+ IsFromHero bool
+ IsNew bool
+ HasHit bool
+ HitAlienLaser bool
+ Points int
+}
+
+func NewHeroLaser(heroGunPosition int, y int) *Laser {
+ return &Laser{
+ Rectangle: tl.NewRectangle(heroGunPosition, y, 1, 1, tl.ColorRed),
+ Direction: 1,
+ IsNew: true,
+ IsFromHero: true,
+ }
+}
+
+func NewAlienLaser(alienGunPosition int, y int) *Laser {
+ return &Laser{
+ Rectangle: tl.NewRectangle(alienGunPosition, y, 1, 1, tl.ColorGreen),
+ Direction: -1,
+ IsNew: true,
+ IsFromHero: false,
+ Points: 5,
+ }
+}
+
+func (laser *Laser) Collide(collision tl.Physical) {
+ if laser.IsFromHero == false {
+ return
+ }
+
+ if laserCollide, isLaser := collision.(*Laser); isLaser {
+ laser.HasHit = true
+ laser.HitAlienLaser = true
+ laser.Points = laserCollide.Points
+
+ laserCollide.HasHit = true
+ }
+}
diff --git a/invaders/screens.go b/invaders/screens.go
new file mode 100644
index 0000000..739ce70
--- /dev/null
+++ b/invaders/screens.go
@@ -0,0 +1,97 @@
+package invaders
+
+import (
+ _ "embed"
+ "fmt"
+ tl "github.com/JoelOtter/termloop"
+)
+
+var (
+ //go:embed files/title.txt
+ titleScreenFile []byte
+
+ //go:embed files/game_over.txt
+ gameOverScreenFile []byte
+)
+
+func ShowTitleScreen(invaders *Invaders) {
+ prepareScreen(invaders)
+ showTitle(invaders)
+
+ if checkArenaSizeNotOk(invaders) {
+ invaders.ScreenSizeNotOK = true
+ showMaximizeScreen(invaders)
+ return
+ }
+
+ showPressToInit(invaders, 0)
+}
+
+func checkArenaSizeNotOk(invaders *Invaders) bool {
+ w, h := invaders.Arena.Size()
+
+ if w < 100 || h < 37 {
+ return true
+ }
+
+ return false
+}
+
+func ShowGameOverScreen(invaders *Invaders) {
+ prepareScreen(invaders)
+ showGameOver(invaders)
+ showScore(invaders)
+ showPressToInit(invaders, 2)
+}
+
+func prepareScreen(invaders *Invaders) {
+ invaders.Level = tl.NewBaseLevel(tl.Cell{Bg: tl.ColorBlack, Fg: tl.ColorWhite})
+ invaders.Game.Screen().SetLevel(invaders.Level)
+ invaders.Level.AddEntity(invaders)
+
+ invaders.initArena()
+ invaders.initHud()
+}
+
+func showTitle(invaders *Invaders) {
+ showCanvas(invaders, titleScreenFile)
+}
+
+func showGameOver(invaders *Invaders) {
+ showCanvas(invaders, gameOverScreenFile)
+}
+
+func showCanvas(invaders *Invaders, file []byte) {
+ canvas := CreateCanvas(file)
+
+ arenaX, arenaY := invaders.Arena.Position()
+ arenaW, arenaH := invaders.Arena.Size()
+
+ x := arenaX + arenaW/2 - len(canvas)/2
+ y := arenaY + arenaH/2 + -len(canvas[0]) - 1
+
+ invaders.Level.AddEntity(tl.NewEntityFromCanvas(x, y, canvas))
+}
+
+func showScore(invaders *Invaders) {
+ score := fmt.Sprintf("SCORE: %4d ", invaders.Score)
+ showCenterText(score, 0, invaders)
+}
+
+func showPressToInit(invaders *Invaders, topPadding int) {
+ showCenterText("Press ENTER to start", topPadding, invaders)
+}
+
+func showMaximizeScreen(invaders *Invaders) {
+ showCenterText("Maximize the console and run the game again", 0, invaders)
+}
+
+func showCenterText(text string, topPadding int, invaders *Invaders) {
+ arenaX, arenaY := invaders.Arena.Position()
+ arenaW, arenaH := invaders.Arena.Size()
+
+ x := arenaX + arenaW/2 - len(text)/2
+ y := arenaY + arenaH/2 + topPadding
+
+ invaders.Level.AddEntity(tl.NewText(x, y, text, tl.ColorWhite, tl.ColorBlack))
+}
diff --git a/invaders/utils/utils.go b/invaders/utils/utils.go
new file mode 100644
index 0000000..cd13004
--- /dev/null
+++ b/invaders/utils/utils.go
@@ -0,0 +1,5 @@
+package utils
+
+func ValueMinusPercent(val int, percentage float64) int {
+ return int(float64(val) - float64(val)*percentage)
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..a802cef
--- /dev/null
+++ b/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "github.com/afagundes/go-invaders/invaders"
+
+func main() {
+ invaders.NewGame().Start()
+}