394 lines
8.9 KiB
Go
394 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"gitlab.com/tslocum/cbind"
|
|
|
|
"github.com/ByteArena/box2d"
|
|
"gitlab.com/tslocum/cview"
|
|
)
|
|
|
|
var app *cview.Application
|
|
|
|
var tv *cview.TextView
|
|
|
|
var actors = make(map[string]*box2d.B2Body)
|
|
|
|
var updates = make(chan func(), 10)
|
|
|
|
var lastAT = time.Time{}
|
|
var lastLT = time.Time{}
|
|
|
|
const moonGravity = -1.625 * .1
|
|
|
|
const thrustMagnitude = 777.7
|
|
|
|
const rotateSpeed = 0.005
|
|
|
|
const debug = false
|
|
|
|
const (
|
|
startAngle = math.Pi / 3
|
|
startLVX = 1
|
|
startLVY = 0
|
|
startX = 15
|
|
startY = 32
|
|
|
|
fuzzStartA = 15 // Angle
|
|
fuzzStartAV = 0.25 // Angular velocity
|
|
fuzzStartX = 32
|
|
fuzzStartY = 3
|
|
|
|
startFuel = 2375
|
|
|
|
thrustBurnRate = 15
|
|
angularBurnRate = 5
|
|
|
|
maxLandingA = 27.0
|
|
maxLandingAV = 13.5
|
|
maxLandingLV = 1.4
|
|
)
|
|
|
|
var (
|
|
playerAngle float64
|
|
playerX, playerY float64
|
|
playerAV float64
|
|
playerLV float64
|
|
playerLanded bool
|
|
|
|
playerFuel int
|
|
|
|
classicMode bool
|
|
gameOver bool
|
|
playerCrashed bool
|
|
)
|
|
|
|
const (
|
|
screenHeight = 32
|
|
screenWidth = 120
|
|
)
|
|
|
|
var stars = map[int]map[int]string{
|
|
4: {7: "*"},
|
|
9: {23: "."},
|
|
11: {42: "."},
|
|
32: {16: "."},
|
|
87: {22: "."},
|
|
58: {28: "*"},
|
|
72: {17: "*"},
|
|
102: {25: "*"},
|
|
96: {9: "."},
|
|
56: {11: "."},
|
|
25: {17: "*"},
|
|
42: {24: "."},
|
|
28: {14: "."},
|
|
}
|
|
|
|
func setupInput() {
|
|
inputConfig := cbind.NewConfiguration()
|
|
|
|
_ = inputConfig.Set("Left", func(ev *tcell.EventKey) *tcell.EventKey {
|
|
updates <- func() {
|
|
if gameOver || playerLanded || playerFuel <= 0 {
|
|
return
|
|
}
|
|
|
|
v := actors["player"].GetAngularVelocity()
|
|
v -= rotateSpeed
|
|
actors["player"].SetAngularVelocity(v)
|
|
lastAT = time.Now()
|
|
playerFuel -= angularBurnRate
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
_ = inputConfig.Set("Right", func(ev *tcell.EventKey) *tcell.EventKey {
|
|
updates <- func() {
|
|
if gameOver || playerLanded || playerFuel <= 0 {
|
|
return
|
|
}
|
|
|
|
v := actors["player"].GetAngularVelocity()
|
|
v += rotateSpeed
|
|
actors["player"].SetAngularVelocity(v)
|
|
lastAT = time.Now()
|
|
playerFuel -= angularBurnRate
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
moveFunc :=
|
|
func() {
|
|
updates <- func() {
|
|
if gameOver || playerFuel <= 0 {
|
|
return
|
|
}
|
|
angle := actors["player"].GetAngle() - math.Pi/2
|
|
//actors["player"].ApplyLinearImpulseToCenter(box2d.B2Vec2{math.Cos(angle), math.Sin(angle)}, true)
|
|
actors["player"].ApplyLinearImpulse(box2d.B2Vec2{math.Sin(angle) * thrustMagnitude, math.Cos(angle) * thrustMagnitude}, actors["player"].GetWorldCenter(), true)
|
|
lastLT = time.Now()
|
|
playerFuel -= thrustBurnRate
|
|
}
|
|
}
|
|
|
|
_ = inputConfig.Set("Up", func(ev *tcell.EventKey) *tcell.EventKey {
|
|
moveFunc()
|
|
return nil
|
|
})
|
|
|
|
_ = inputConfig.Set("Space", func(ev *tcell.EventKey) *tcell.EventKey {
|
|
moveFunc()
|
|
return nil
|
|
})
|
|
|
|
_ = inputConfig.Set("Escape", func(ev *tcell.EventKey) *tcell.EventKey {
|
|
app.Stop()
|
|
return nil
|
|
})
|
|
|
|
app.SetInputCapture(inputConfig.Capture)
|
|
}
|
|
|
|
func renderPoint(area int, x int, y int) string {
|
|
sx, ok := stars[x]
|
|
if ok {
|
|
sy, ok := sx[y]
|
|
if ok {
|
|
return sy
|
|
}
|
|
}
|
|
return " "
|
|
}
|
|
|
|
func renderWorld() {
|
|
px, py := int(math.Floor(playerX)), int(math.Floor(playerY))
|
|
a := playerAngle
|
|
var buf bytes.Buffer
|
|
for y := screenHeight - 1; y >= 0; y-- {
|
|
if y != screenHeight-1 {
|
|
buf.Write([]byte("\n"))
|
|
}
|
|
for x := 0; x < screenWidth; x++ {
|
|
if x == px && y == py {
|
|
if playerCrashed {
|
|
buf.Write([]byte("x"))
|
|
} else if a > 337.5 || a < 22.5 {
|
|
buf.Write([]byte("⬆"))
|
|
} else if a < 67.5 {
|
|
buf.Write([]byte("⬈"))
|
|
} else if a < 112.5 {
|
|
buf.Write([]byte("➡"))
|
|
} else if a < 157.5 {
|
|
buf.Write([]byte("⬊"))
|
|
} else if a < 202.5 {
|
|
buf.Write([]byte("⬇"))
|
|
} else if a < 247.5 {
|
|
buf.Write([]byte("⬋"))
|
|
} else if a < 292.5 {
|
|
buf.Write([]byte("⬅"))
|
|
} else {
|
|
buf.Write([]byte("⬉"))
|
|
}
|
|
continue
|
|
}
|
|
|
|
buf.Write([]byte(renderPoint(1, x, y)))
|
|
}
|
|
}
|
|
|
|
buf.Write([]byte("\n"))
|
|
buf.Write([]byte("[gray]███▓████████████████▓▓▓██████▓▓████████▓▓▓████████▓▓▓▓█████████████▓▓████████▓████▓▓███████████▓▓██████▓▓▓▓▓██████████[-]"))
|
|
buf.Write([]byte("\n"))
|
|
buf.Write([]byte("\n"))
|
|
buf.Write([]byte(strings.Repeat(" ", 22)))
|
|
if gameOver {
|
|
if playerCrashed {
|
|
buf.Write([]byte(" [ CRASHED ] "))
|
|
} else {
|
|
buf.Write([]byte(" [ LANDED ] "))
|
|
}
|
|
} else if playerFuel <= 0 {
|
|
buf.Write([]byte(" [ OUT OF FUEL ] "))
|
|
} else {
|
|
if time.Since(lastAT) < 150*time.Millisecond {
|
|
buf.Write([]byte(" [ ROTATIONAL THRUSTERS ] "))
|
|
} else {
|
|
buf.Write([]byte(strings.Repeat(" ", 27)))
|
|
}
|
|
if time.Since(lastLT) < 150*time.Millisecond {
|
|
buf.Write([]byte(" [ POSITIONAL THRUSTERS ] "))
|
|
} else {
|
|
buf.Write([]byte(strings.Repeat(" ", 27)))
|
|
}
|
|
}
|
|
buf.Write([]byte("\n"))
|
|
buf.Write([]byte("\n"))
|
|
buf.Write([]byte(fmt.Sprintf(" Fuel: %d.0 kg - Speed: %.01f m/s - Rotation speed: %.01f deg/s - Rotation angle: %.01f deg", playerFuel, playerLV, playerAV, playerAngle)))
|
|
|
|
tv.SetBytes(buf.Bytes())
|
|
}
|
|
|
|
func runSimulation() {
|
|
playerFuel = startFuel
|
|
|
|
// Define the gravity vector.
|
|
gravity := box2d.MakeB2Vec2(0.0, moonGravity)
|
|
|
|
// Construct a world object, which will hold and simulate the rigid bodies.
|
|
world := box2d.MakeB2World(gravity)
|
|
|
|
{
|
|
bd := box2d.MakeB2BodyDef()
|
|
ground := world.CreateBody(&bd)
|
|
|
|
shape := box2d.MakeB2EdgeShape()
|
|
shape.Set(box2d.MakeB2Vec2(-20000.0, 0.0), box2d.MakeB2Vec2(20000.0, -1.0))
|
|
ground.CreateFixture(&shape, 0.0)
|
|
actors["ground"] = ground
|
|
}
|
|
|
|
// Circle character
|
|
{
|
|
bd := box2d.MakeB2BodyDef()
|
|
bd.Position.Set(startX, startY)
|
|
bd.LinearVelocity.Set(startLVX, startLVY)
|
|
bd.Type = box2d.B2BodyType.B2_dynamicBody
|
|
bd.FixedRotation = true
|
|
bd.AllowSleep = false
|
|
|
|
angle := startAngle
|
|
if !classicMode {
|
|
angle += float64(rand.Intn(fuzzStartA))
|
|
bd.AngularVelocity += fuzzStartAV - (float64(rand.Intn(fuzzStartAV*200)) / 100)
|
|
bd.Position.X += float64(rand.Intn(fuzzStartX))
|
|
bd.Position.Y -= float64(rand.Intn(fuzzStartY))
|
|
}
|
|
|
|
body := world.CreateBody(&bd)
|
|
body.SetTransform(body.GetPosition(), angle)
|
|
|
|
shape := box2d.MakeB2CircleShape()
|
|
shape.M_radius = 0.5
|
|
|
|
fd := box2d.MakeB2FixtureDef()
|
|
fd.Shape = &shape
|
|
fd.Density = 15103
|
|
body.CreateFixtureFromDef(&fd)
|
|
actors["player"] = body
|
|
}
|
|
|
|
// Prepare for simulation. Typically we use a time step of 1/60 of a
|
|
// second (60Hz) and 10 iterations. This provides a high quality simulation
|
|
// in most game scenarios.
|
|
timeStep := 1.0 / 60.0
|
|
velocityIterations := 8
|
|
positionIterations := 3
|
|
|
|
t := time.NewTicker(time.Second / 60)
|
|
for {
|
|
UPDATELOOP:
|
|
for {
|
|
select {
|
|
case f := <-updates:
|
|
f()
|
|
default:
|
|
break UPDATELOOP
|
|
}
|
|
}
|
|
|
|
WAITLOOP:
|
|
for {
|
|
select {
|
|
case f := <-updates:
|
|
f()
|
|
case <-t.C:
|
|
break WAITLOOP
|
|
}
|
|
}
|
|
|
|
if gameOver {
|
|
app.QueueUpdateDraw(renderWorld)
|
|
for {
|
|
select {
|
|
case <-updates:
|
|
case <-t.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
// Instruct the world to perform a single step of simulation.
|
|
// It is generally best to keep the time step and iterations fixed.
|
|
//runtime.Breakpoint()
|
|
world.Step(timeStep, velocityIterations, positionIterations)
|
|
|
|
playerAngle = math.Mod(((actors["player"].GetAngle() - 0.5*math.Pi) * (180 / math.Pi)), 360)
|
|
if playerAngle < 0 {
|
|
playerAngle = 360 + playerAngle
|
|
}
|
|
|
|
position := actors["player"].GetPosition()
|
|
playerX, playerY = position.X, position.Y
|
|
playerAV = actors["player"].GetAngularVelocity() * (180 / math.Pi)
|
|
playerLV = actors["player"].GetLinearVelocity().Y
|
|
|
|
list := actors["player"].GetContactList()
|
|
playerLanded = list != nil
|
|
if playerLanded && !debug {
|
|
av := actors["player"].GetAngularVelocity()
|
|
lv := actors["player"].GetLinearVelocity()
|
|
playerCrashed = av > maxLandingAV || av < -maxLandingAV || lv.X > maxLandingLV || lv.X < -maxLandingLV || lv.Y > maxLandingLV || lv.Y < -maxLandingLV
|
|
if !playerCrashed {
|
|
crashAngle := math.Mod(((actors["player"].GetAngle() - 0.5*math.Pi) * (180 / math.Pi)), 360)
|
|
playerCrashed = crashAngle > maxLandingA || crashAngle < -maxLandingA
|
|
}
|
|
|
|
gameOver = true
|
|
actors["player"].SetAngularVelocity(0)
|
|
actors["player"].SetLinearVelocity(box2d.MakeB2Vec2(0, 0))
|
|
}
|
|
|
|
app.QueueUpdateDraw(renderWorld)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
app = cview.NewApplication()
|
|
|
|
err := app.Init()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
w, h := app.GetScreenSize()
|
|
if w < screenWidth || h < screenHeight+4 {
|
|
log.Fatalf("failed to start basiclander: insufficient terminal dimensions: requires %dx%d: have %dx%d", screenWidth, screenHeight+4, w, h)
|
|
}
|
|
|
|
setupInput()
|
|
|
|
tv = cview.NewTextView()
|
|
tv.SetDynamicColors(true)
|
|
tv.SetScrollBarVisibility(cview.ScrollBarNever)
|
|
tv.SetWrap(false)
|
|
tv.SetWordWrap(false)
|
|
|
|
go runSimulation()
|
|
|
|
app.SetRoot(tv, true)
|
|
|
|
if err := app.Run(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|