rove/pkg/game/world.go
Marc Di Luzio b116cdf291 Convert Atlas to infinite lazy growth
The atlas will now expand as needed for any query, but only initialise the chunk tile memory when requested

	While this may still be a pre-mature optimisation, it does simplify some code and ensures that our memory footprint stays small, for the most part
2020-06-27 14:48:21 +01:00

461 lines
11 KiB
Go

package game
import (
"bufio"
"fmt"
"log"
"math"
"math/rand"
"os"
"sync"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/objects"
"github.com/mdiluz/rove/pkg/vector"
)
// World describes a self contained universe and everything in it
type World struct {
// Rovers is a id->data map of all the rovers in the game
Rovers map[string]Rover `json:"rovers"`
// Atlas represends the world map of chunks and tiles
Atlas atlas.Atlas `json:"atlas"`
// Mutex to lock around all world operations
worldMutex sync.RWMutex
// Commands is the set of currently executing command streams per rover
CommandQueue map[string]CommandStream `json:"commands"`
// Incoming represents the set of commands to add to the queue at the end of the current tick
Incoming map[string]CommandStream `json:"incoming"`
// Mutex to lock around command operations
cmdMutex sync.RWMutex
// Set of possible words to use for names
words []string
}
var wordsFile = os.Getenv("WORDS_FILE")
// NewWorld creates a new world object
func NewWorld(chunkSize int) *World {
// Try and load the words file
var lines []string
if file, err := os.Open(wordsFile); err != nil {
log.Printf("Couldn't read words file [%s], running without words: %s\n", wordsFile, err)
} else {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if scanner.Err() != nil {
log.Printf("Failure during word file scan: %s\n", scanner.Err())
}
}
return &World{
Rovers: make(map[string]Rover),
CommandQueue: make(map[string]CommandStream),
Incoming: make(map[string]CommandStream),
Atlas: atlas.NewAtlas(chunkSize),
words: lines,
}
}
// SpawnRover adds an rover to the game
func (w *World) SpawnRover() (string, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
// Initialise the rover
rover := Rover{
Range: 4.0,
Integrity: 10,
Name: uuid.New().String(),
}
// Assign a random name if we have words
if len(w.words) > 0 {
for {
// Loop until we find a unique name
name := fmt.Sprintf("%s-%s", w.words[rand.Intn(len(w.words))], w.words[rand.Intn(len(w.words))])
if _, ok := w.Rovers[name]; !ok {
rover.Name = name
break
}
}
}
// Spawn in a random place near the origin
rover.Pos = vector.Vector{
X: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
Y: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
}
// Seach until we error (run out of world)
for {
tile := w.Atlas.GetTile(rover.Pos)
if !objects.IsBlocking(tile) {
break
} else {
// Try and spawn to the east of the blockage
rover.Pos.Add(vector.Vector{X: 1, Y: 0})
}
}
log.Printf("Spawned rover at %+v\n", rover.Pos)
// Append the rover to the list
w.Rovers[rover.Name] = rover
return rover.Name, nil
}
// GetRover gets a specific rover by name
func (w *World) GetRover(rover string) (Rover, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if i, ok := w.Rovers[rover]; ok {
return i, nil
} else {
return Rover{}, fmt.Errorf("Failed to find rover with name: %s", rover)
}
}
// Removes an rover from the game
func (w *World) DestroyRover(rover string) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[rover]; ok {
// Clear the tile
w.Atlas.SetTile(i.Pos, objects.Empty)
delete(w.Rovers, rover)
} else {
return fmt.Errorf("no rover matching id")
}
return nil
}
// RoverPosition returns the position of the rover
func (w *World) RoverPosition(rover string) (vector.Vector, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if i, ok := w.Rovers[rover]; ok {
return i.Pos, nil
} else {
return vector.Vector{}, fmt.Errorf("no rover matching id")
}
}
// SetRoverPosition sets the position of the rover
func (w *World) SetRoverPosition(rover string, pos vector.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[rover]; ok {
i.Pos = pos
w.Rovers[rover] = i
return nil
} else {
return fmt.Errorf("no rover matching id")
}
}
// RoverInventory returns the inventory of a requested rover
func (w *World) RoverInventory(rover string) ([]byte, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if i, ok := w.Rovers[rover]; ok {
return i.Inventory, nil
} else {
return nil, fmt.Errorf("no rover matching id")
}
}
// WarpRover sets an rovers position
func (w *World) WarpRover(rover string, pos vector.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[rover]; ok {
// Nothing to do if these positions match
if i.Pos == pos {
return nil
}
// Check the tile is not blocked
tile := w.Atlas.GetTile(pos)
if objects.IsBlocking(tile) {
return fmt.Errorf("can't warp rover to occupied tile, check before warping")
}
i.Pos = pos
w.Rovers[rover] = i
return nil
} else {
return fmt.Errorf("no rover matching id")
}
}
// SetPosition sets an rovers position
func (w *World) MoveRover(rover string, b bearing.Bearing) (vector.Vector, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[rover]; ok {
// Try the new move position
newPos := i.Pos.Added(b.Vector())
// Get the tile and verify it's empty
tile := w.Atlas.GetTile(newPos)
if !objects.IsBlocking(tile) {
// Perform the move
i.Pos = newPos
w.Rovers[rover] = i
} else {
// If it is a blocking tile, reduce the rover integrity
i.Integrity = i.Integrity - 1
if i.Integrity == 0 {
// TODO: The rover needs to be left dormant with the player
} else {
w.Rovers[rover] = i
}
}
return i.Pos, nil
} else {
return vector.Vector{}, fmt.Errorf("no rover matching id")
}
}
// RoverStash will stash an item at the current rovers position
func (w *World) RoverStash(rover string) (byte, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if r, ok := w.Rovers[rover]; ok {
tile := w.Atlas.GetTile(r.Pos)
if objects.IsStashable(tile) {
r.Inventory = append(r.Inventory, tile)
w.Rovers[rover] = r
w.Atlas.SetTile(r.Pos, objects.Empty)
return tile, nil
}
} else {
return objects.Empty, fmt.Errorf("no rover matching id")
}
return objects.Empty, nil
}
// RadarFromRover can be used to query what a rover can currently see
func (w *World) RadarFromRover(rover string) ([]byte, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if r, ok := w.Rovers[rover]; ok {
// The radar should span in range direction on each axis, plus the row/column the rover is currently on
radarSpan := (r.Range * 2) + 1
roverPos := r.Pos
// Get the radar min and max values
radarMin := vector.Vector{
X: roverPos.X - r.Range,
Y: roverPos.Y - r.Range,
}
radarMax := vector.Vector{
X: roverPos.X + r.Range,
Y: roverPos.Y + r.Range,
}
// Gather up all tiles within the range
var radar = make([]byte, radarSpan*radarSpan)
for j := radarMin.Y; j <= radarMax.Y; j++ {
for i := radarMin.X; i <= radarMax.X; i++ {
q := vector.Vector{X: i, Y: j}
tile := w.Atlas.GetTile(q)
// Get the position relative to the bottom left of the radar
relative := q.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
radar[index] = tile
}
}
// Add all rovers to the radar
for _, r := range w.Rovers {
// If the rover is in range
dist := r.Pos.Added(roverPos.Negated())
dist = dist.Abs()
if dist.X <= r.Range && dist.Y <= r.Range {
relative := r.Pos.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
radar[index] = objects.Rover
}
}
// Add this rover
radar[len(radar)/2] = objects.Rover
return radar, nil
} else {
return nil, fmt.Errorf("no rover matching id")
}
}
// Enqueue will queue the commands given
func (w *World) Enqueue(rover string, commands ...Command) error {
// First validate the commands
for _, c := range commands {
switch c.Command {
case CommandMove:
if _, err := bearing.FromString(c.Bearing); err != nil {
return fmt.Errorf("unknown bearing: %s", c.Bearing)
}
case CommandStash:
case CommandRepair:
// Nothing to verify
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
}
// Lock our commands edit
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Append the commands to the incoming set
if cmds, ok := w.Incoming[rover]; ok {
w.Incoming[rover] = append(cmds, commands...)
} else {
w.Incoming[rover] = commands
}
return nil
}
// EnqueueAllIncoming will enqueue the incoming commands
func (w *World) EnqueueAllIncoming() {
// Add any incoming commands from this tick and clear that queue
for id, incoming := range w.Incoming {
commands := w.CommandQueue[id]
commands = append(commands, incoming...)
w.CommandQueue[id] = commands
}
w.Incoming = make(map[string]CommandStream)
}
// Execute will execute any commands in the current command queue
func (w *World) ExecuteCommandQueues() {
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Iterate through all the current commands
for rover, cmds := range w.CommandQueue {
if len(cmds) != 0 {
// Extract the first command in the queue
c := cmds[0]
w.CommandQueue[rover] = cmds[1:]
// Execute the command
if err := w.ExecuteCommand(&c, rover); err != nil {
log.Println(err)
// TODO: Report this error somehow
}
} else {
// Clean out the empty entry
delete(w.CommandQueue, rover)
}
}
// Add any incoming commands from this tick and clear that queue
w.EnqueueAllIncoming()
}
// ExecuteCommand will execute a single command
func (w *World) ExecuteCommand(c *Command, rover string) (err error) {
log.Printf("Executing command: %+v for %s\n", *c, rover)
switch c.Command {
case CommandMove:
if dir, err := bearing.FromString(c.Bearing); err != nil {
return err
} else if _, err := w.MoveRover(rover, dir); err != nil {
return err
}
case CommandStash:
if _, err := w.RoverStash(rover); err != nil {
return err
}
case CommandRepair:
if r, err := w.GetRover(rover); err != nil {
return err
} else {
// Consume an inventory item to repair
if len(r.Inventory) > 0 {
r.Inventory = r.Inventory[:len(r.Inventory)-1]
r.Integrity = r.Integrity + 1
w.Rovers[rover] = r
}
}
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
return
}
// PrintTiles simply prints the input tiles directly for debug
func PrintTiles(tiles []byte) {
num := int(math.Sqrt(float64(len(tiles))))
for j := num - 1; j >= 0; j-- {
for i := 0; i < num; i++ {
fmt.Printf("%c", tiles[i+num*j])
}
fmt.Print("\n")
}
}
// RLock read locks the world
func (w *World) RLock() {
w.worldMutex.RLock()
w.cmdMutex.RLock()
}
// RUnlock read unlocks the world
func (w *World) RUnlock() {
w.worldMutex.RUnlock()
w.cmdMutex.RUnlock()
}
// Lock locks the world
func (w *World) Lock() {
w.worldMutex.Lock()
w.cmdMutex.Lock()
}
// Unlock unlocks the world
func (w *World) Unlock() {
w.worldMutex.Unlock()
w.cmdMutex.Unlock()
}