package game import ( "bufio" "fmt" "log" "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, MaximumIntegrity: 10, Capacity: 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 { _, obj := w.Atlas.QueryPosition(rover.Pos) if !obj.IsBlocking() { 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() i, ok := w.Rovers[rover] if !ok { return Rover{}, fmt.Errorf("Failed to find rover with name: %s", rover) } return i, nil } // DestroyRover Removes an rover from the game func (w *World) DestroyRover(rover string) error { w.worldMutex.Lock() defer w.worldMutex.Unlock() _, ok := w.Rovers[rover] if !ok { return fmt.Errorf("no rover matching id") } delete(w.Rovers, rover) 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() i, ok := w.Rovers[rover] if !ok { return vector.Vector{}, fmt.Errorf("no rover matching id") } return i.Pos, nil } // SetRoverPosition sets the position of the rover func (w *World) SetRoverPosition(rover string, pos vector.Vector) error { w.worldMutex.Lock() defer w.worldMutex.Unlock() i, ok := w.Rovers[rover] if !ok { return fmt.Errorf("no rover matching id") } i.Pos = pos w.Rovers[rover] = i return nil } // RoverInventory returns the inventory of a requested rover func (w *World) RoverInventory(rover string) ([]objects.Object, error) { w.worldMutex.RLock() defer w.worldMutex.RUnlock() i, ok := w.Rovers[rover] if !ok { return nil, fmt.Errorf("no rover matching id") } return i.Inventory, nil } // WarpRover sets an rovers position func (w *World) WarpRover(rover string, pos vector.Vector) error { w.worldMutex.Lock() defer w.worldMutex.Unlock() i, ok := w.Rovers[rover] if !ok { return fmt.Errorf("no rover matching id") } // Nothing to do if these positions match if i.Pos == pos { return nil } // Check the tile is not blocked _, obj := w.Atlas.QueryPosition(pos) if obj.IsBlocking() { return fmt.Errorf("can't warp rover to occupied tile, check before warping") } i.Pos = pos w.Rovers[rover] = i return nil } // MoveRover attempts to move a rover in a specific direction func (w *World) MoveRover(rover string, b bearing.Bearing) (vector.Vector, error) { w.worldMutex.Lock() defer w.worldMutex.Unlock() i, ok := w.Rovers[rover] if !ok { return vector.Vector{}, fmt.Errorf("no rover matching id") } // Try the new move position newPos := i.Pos.Added(b.Vector()) // Get the tile and verify it's empty _, obj := w.Atlas.QueryPosition(newPos) if !obj.IsBlocking() { // 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 } // RoverStash will stash an item at the current rovers position func (w *World) RoverStash(rover string) (objects.Type, error) { w.worldMutex.Lock() defer w.worldMutex.Unlock() r, ok := w.Rovers[rover] if !ok { return objects.None, fmt.Errorf("no rover matching id") } // Can't pick up when full if len(r.Inventory) >= r.Capacity { return objects.None, nil } _, obj := w.Atlas.QueryPosition(r.Pos) if !obj.IsStashable() { return objects.None, nil } r.Inventory = append(r.Inventory, obj) w.Rovers[rover] = r w.Atlas.SetObject(r.Pos, objects.Object{Type: objects.None}) return obj.Type, nil } // RadarFromRover can be used to query what a rover can currently see func (w *World) RadarFromRover(rover string) (radar []byte, objs []byte, err error) { w.worldMutex.RLock() defer w.worldMutex.RUnlock() r, ok := w.Rovers[rover] if !ok { err = fmt.Errorf("no rover matching id") return } // 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 radar = make([]byte, radarSpan*radarSpan) objs = 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, obj := w.Atlas.QueryPosition(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 objs[index] = byte(obj.Type) } } // 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 objs[index] = byte(objects.Rover) } } return radar, objs, nil } // 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) } // ExecuteCommandQueues 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: r, err := w.GetRover(rover) if err != nil { return err } // Consume an inventory item to repair if possible if len(r.Inventory) > 0 && r.Integrity < r.MaximumIntegrity { 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 } // 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() }