├── .gitignore ├── go.mod ├── gogue.go ├── ecs ├── utils.go ├── components.go ├── systems.go ├── systemsMessages.go ├── controller.go └── ecs_test.go ├── hooks └── pre-commit ├── ui ├── utils.go ├── glyph.go ├── input.go ├── ui_test.go ├── messageLog.go ├── menu.go └── terminal.go ├── data ├── testdata │ ├── enemies.json │ └── enemies_2.json ├── dataLoader.go ├── data_test.go └── entityLoader.go ├── noises ├── noiseDegradation.go └── noiseGeneration.go ├── .github └── workflows │ └── go.yml ├── gamemap ├── maptypes │ ├── arena.go │ ├── maptypes_test.go │ └── cavern.go ├── gamemap_test.go └── map.go ├── randomnumbergenerator ├── dice.go └── rng.go ├── README.md ├── fov └── fov.go ├── camera ├── camera_test.go └── camera.go ├── screens ├── screen.go └── screens_test.go └── djikstramaps ├── multiEntityMap.go └── entityMap.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea* 2 | go.sum 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gogue-framework/gogue 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/gogue-framework/bearlibterminalgo v1.0.1 7 | github.com/stretchr/testify v1.5.1 8 | ) 9 | -------------------------------------------------------------------------------- /gogue.go: -------------------------------------------------------------------------------- 1 | package gogue 2 | 3 | // This is silly, but it seems you can't build a package without a go file in the root of the package. I'm sure there's 4 | // something I'm not aware of/mis-understanding here, but until such time as I figure that out, I guess this file will 5 | // stay here. 6 | -------------------------------------------------------------------------------- /ecs/utils.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "reflect" 4 | 5 | // IntInSlice will return true if the integer value provided is present in the slice provided, false otherwise. 6 | func IntInSlice(a int, list []int) bool { 7 | for _, b := range list { 8 | if b == a { 9 | return true 10 | } 11 | } 12 | return false 13 | } 14 | 15 | // TypeInSlice will return true if the reflect.Type provided is present in the slice provided, false otherwise. 16 | func TypeInSlice(a reflect.Type, list []reflect.Type) bool { 17 | for _, b := range list { 18 | if b == a { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | -------------------------------------------------------------------------------- /ecs/components.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // Component is a metadata container used to house information about something an entities state. 8 | // Example Components might look like: 9 | // type PositionComponent struct { 10 | // X int 11 | // Y int 12 | // } 13 | // This position component represents where an entity is in the game world. If an entity does not have a position 14 | // component, it can be assumed they are not present in the world. 15 | // Another type of component might look like: 16 | // type CanAttackComponent {} 17 | // CanAttackComponent has no data attached, and acts merely as a flag. If an entity has this component, they can attack 18 | // if an entity is missing this component, they cannot attack. 19 | // Components are a flexible way of attaching metadata to an entity. 20 | type Component interface { 21 | TypeOf() reflect.Type 22 | } 23 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Pre-commit checks..." 4 | 5 | FILES=$(go list ./... | grep -v /vendor/) 6 | 7 | echo "go fmt" 8 | go fmt ${FILES} 9 | echo "success!" 10 | echo "" 11 | 12 | echo "go vet" 13 | # Check all files for suspicious constructs 14 | { 15 | go vet ${FILES} 16 | } || { 17 | exitStatus=$? 18 | 19 | if [ $exitStatus ]; then 20 | printf "\ngo vet issues found in your code, please fix them and try again." 21 | exit 1 22 | fi 23 | } 24 | echo "success!" 25 | echo "" 26 | 27 | echo "go lint" 28 | # Lint the entire project. Requires golint (https://github.com/golang/lint) 29 | failed=false 30 | 31 | for file in ${FILES}; do 32 | # redirect stderr so that violations and summaries are properly interleaved. 33 | if ! golint -set_exit_status "$file" 2>&1 34 | then 35 | failed=true 36 | fi 37 | done 38 | 39 | if [[ $failed == "true" ]]; then 40 | exit 1 41 | fi 42 | echo "success!" 43 | echo "" 44 | 45 | echo "Pre-commit checks successful!" 46 | -------------------------------------------------------------------------------- /ecs/systems.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | // System operates on entities to produce a result. A system is a method of modifying the game world and its contained 4 | // entities in a manner based on the state of the entities (read: what components that have, and what data those 5 | // components contain). A system might move all monster entities that have a MovementComponent, or calcualte poison 6 | // damage for all entities that a PoisonedComponent. They can also be used to allow the player to move: if the player 7 | // has a movement component, the system should accept movement input, and adjust the players position component 8 | // accordingly. If the player is missing a movement component (say, they have been paralyzed), the system should not 9 | // accept input from the user, and should skip the users turn entirely. 10 | // Each system has a Process method that contains the system logic. This Process method will be called when the system 11 | // is processed by the Controller. 12 | type System interface { 13 | Process() 14 | } 15 | -------------------------------------------------------------------------------- /ui/utils.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // SplitLines takes a text string, and splits into lines that do not exceed the character count represented by width 9 | // This function will try to split on word boundaries 10 | func SplitLines(text string, width int) []string { 11 | var lines []string 12 | 13 | line := "" 14 | 15 | wordList := strings.Fields(text) 16 | 17 | for _, word := range wordList { 18 | if len(line) < width { 19 | // If the line is still less than the total width, attempt to add the new word 20 | tempLine := "" 21 | if len(line) == 0 { 22 | tempLine = word 23 | } else { 24 | tempLine = fmt.Sprintf("%s %s", line, word) 25 | } 26 | 27 | // Check the length of the new line. If it is still less than width, set line to the temp value. If the 28 | // length exceeds width, append line to the lines list, and start a new line with the current word 29 | if len(tempLine) < width { 30 | line = tempLine 31 | } else { 32 | lines = append(lines, line) 33 | line = word 34 | } 35 | } 36 | } 37 | 38 | // Append whatever was left 39 | lines = append(lines, line) 40 | 41 | return lines 42 | } 43 | -------------------------------------------------------------------------------- /data/testdata/enemies.json: -------------------------------------------------------------------------------- 1 | { 2 | "level_1": { 3 | "small_rat": { 4 | "components": { 5 | "position": {}, 6 | "appearance": { 7 | "Name": "Small rat", 8 | "Description": "A very small rat. It doesn't look very tough.", 9 | "Glyph": { 10 | "Char": "r", 11 | "Color": "brown" 12 | }, 13 | "Layer": 1 14 | } 15 | } 16 | }, 17 | "large_rat": { 18 | "components": { 19 | "position": {}, 20 | "appearance": { 21 | "Name": "Large rat", 22 | "Description": "A very large rat. It's big...but still doesn't look too tough.", 23 | "Glyph": { 24 | "Char": "R", 25 | "Color": "brown" 26 | }, 27 | "Layer": 1 28 | } 29 | } 30 | }, 31 | "cave_bat": { 32 | "components": { 33 | "position": {}, 34 | "appearance": { 35 | "Name": "Cave bat", 36 | "Description": "A small, gray, bat. It's fast, bot not a threat. By itself.", 37 | "Glyph": { 38 | "Char": "b", 39 | "Color": "gray" 40 | }, 41 | "Layer": 1 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /ui/glyph.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | // Glyph represents a single character that can be drawn to the terminal. Char is the physical character representation, 4 | // Color is the display color, and ExploredColor is the color it is shown when not in direct view (usually a darker 5 | // shade of its Color) 6 | type Glyph interface { 7 | Char() string 8 | Color() string 9 | ExploredColor() string 10 | } 11 | 12 | type glyph struct { 13 | char string 14 | color string 15 | unexploredColor string 16 | } 17 | 18 | func (g glyph) Char() string { 19 | return g.char 20 | } 21 | 22 | func (g glyph) Color() string { 23 | return g.color 24 | } 25 | 26 | func (g glyph) ExploredColor() string { 27 | return g.unexploredColor 28 | } 29 | 30 | // NewGlyph returns a new Glyph object. If no ExploredColor is provided, it is set to gray. 31 | func NewGlyph(char string, color, exploredColor string) Glyph { 32 | if exploredColor == "" { 33 | exploredColor = "gray" 34 | } 35 | return glyph{char, color, exploredColor} 36 | } 37 | 38 | // EmptyGlyph is a glyph with an empty string for its Char. This empty glyph is useful for erasing other glyphs, by 39 | // replacing them with an empty glyph (which will show as nothing on the terminal). 40 | var EmptyGlyph = NewGlyph(" ", "white", "") 41 | -------------------------------------------------------------------------------- /noises/noiseDegradation.go: -------------------------------------------------------------------------------- 1 | package noises 2 | 3 | import "github.com/gogue-framework/gogue/gamemap" 4 | 5 | // DegradeNoises iterates over every tile on the map, and reduces the amount of noise on each tile by a set amount. 6 | // If a noise reaches an intensity of 0, the noise is removed from the tile. This is intended to be run each frame. 7 | // This simulates sounds being intially made, and maybe echoing around for a brief time, but then eventually disappearing 8 | // An example would be if an entity is tracking another entity by the sound its generating, if the tracked entity stops 9 | // generating sounds, eventually, the tracking entity will no longer be able to follow the trail of sound, as the sound 10 | // no longer exists. 11 | func DegradeNoises(mapSurface *gamemap.GameMap, degradationRate float64) { 12 | for x := 0; x < mapSurface.Width; x++ { 13 | for y := 0; y < mapSurface.Height; y++ { 14 | if mapSurface.HasNoises(x, y) { 15 | updatedNoises := make(map[int]float64) 16 | for entity, noise := range mapSurface.Tiles[x][y].Noises { 17 | updatedNoise := noise - degradationRate 18 | 19 | if updatedNoise > 0 { 20 | updatedNoises[entity] = updatedNoise 21 | } 22 | } 23 | 24 | mapSurface.Tiles[x][y].Noises = updatedNoises 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.13 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.13 20 | id: go 21 | 22 | - name: Install BearLibTerminal lib 23 | run: | 24 | wget -O blt.tar.bz2 http://foo.wyrd.name/_media/en:bearlibterminal:bearlibterminal_0.15.7.tar.bz2 25 | tar xf blt.tar.bz2 26 | sudo cp BearLibTerminal_0.15.7/Linux64/libBearLibTerminal.so /usr/lib 27 | sudo ldconfig -n -v /usr/lib 28 | rm -rf BearLibTerminal_0.15.7 29 | rm blt.tar.bz2 30 | 31 | - name: Check out code into the Go module directory 32 | uses: actions/checkout@v2 33 | 34 | - name: Get dependencies 35 | run: | 36 | go get -v -t -d ./... 37 | if [ -f Gopkg.toml ]; then 38 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 39 | dep ensure 40 | fi 41 | 42 | - name: Build 43 | run: go build -v . 44 | 45 | - name: Run tests 46 | run: go test ./... 47 | 48 | - name: Go report card 49 | uses: creekorful/goreportcard-action@v1.0 50 | -------------------------------------------------------------------------------- /gamemap/maptypes/arena.go: -------------------------------------------------------------------------------- 1 | package maptypes 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/gamemap" 5 | "github.com/gogue-framework/gogue/ui" 6 | ) 7 | 8 | // GenerateArena takes a gamemap.Map object, and creates a giant room, ringed with walls. This is a very simple type of 9 | // map that contains no features other than the walls. 10 | func GenerateArena(surface *gamemap.GameMap, wallGlyph, floorGlyph ui.Glyph) { 11 | // Generates a large, empty room, with walls ringing the outside edges 12 | for x := 0; x <= surface.Width; x++ { 13 | for y := 0; y <= surface.Height; y++ { 14 | // All Tiles are created visible, by default. It is left up to the developer to set Tiles to not visible 15 | // as they see fit (say, through use of the FoV tools in Gogue). 16 | if x == 0 || x == surface.Width-1 || y == 0 || y == surface.Height-1 { 17 | surface.Tiles[x][y] = &gamemap.Tile{Glyph: wallGlyph, Blocked: true, BlocksSight: true, Visited: false, Explored: false, Visible: true, X: x, Y: y} 18 | } else { 19 | surface.Tiles[x][y] = &gamemap.Tile{Glyph: floorGlyph, Blocked: false, BlocksSight: false, Visited: false, Explored: false, Visible: true, X: x, Y: y} 20 | 21 | // Add the tile to the list of floor tiles that have been created. This will be used to add items, 22 | // monsters, the player, etc 23 | surface.FloorTiles = append(surface.FloorTiles, surface.Tiles[x][y]) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /randomnumbergenerator/dice.go: -------------------------------------------------------------------------------- 1 | package randomnumbergenerator 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // DiceRoller contains various methods of rolling different types of dice. It uses a Gogue RNG to determine randomness 9 | type DiceRoller struct { 10 | rng RNG 11 | } 12 | 13 | // NewDiceRoller creates a new DiceRoller. It sets the seed for the contained RNG to the current UNIX timestamp 14 | func NewDiceRoller() *DiceRoller { 15 | diceRoller := DiceRoller{} 16 | 17 | // Set the seed to the current time. This can be updated later by the user. 18 | diceRoller.rng.seed = time.Now().UTC().UnixNano() 19 | diceRoller.rng.rand = rand.New(rand.NewSource(diceRoller.rng.seed)) 20 | 21 | return &diceRoller 22 | } 23 | 24 | // GetSeed returns the RNG seed 25 | func (diceRoller *DiceRoller) GetSeed() int64 { 26 | return diceRoller.rng.seed 27 | } 28 | 29 | // SetSeed sets the RNG seed 30 | func (diceRoller *DiceRoller) SetSeed(seed int64) { 31 | diceRoller.rng.seed = seed 32 | } 33 | 34 | // RollNSidedDie rolls a die with N sides 35 | func (diceRoller *DiceRoller) RollNSidedDie(n int) int { 36 | return diceRoller.rng.Range(0, n) + 1 37 | } 38 | 39 | // RollNSidedDieOpenEnded rolls a die with N sides. If the maximum value of the die is rolled, the value is accumulated, 40 | // and the die is rolled again. Any time the max die value is rolled, this process is repeated. This is useful in 41 | // situations where something should be improbable, rather than impossible. 42 | func (diceRoller *DiceRoller) RollNSidedDieOpenEnded(n int) int { 43 | roll := diceRoller.rng.Range(0, n) + 1 44 | totalRoll := roll 45 | 46 | for roll == n { 47 | roll = diceRoller.rng.Range(0, n) + 1 48 | totalRoll += roll 49 | } 50 | 51 | return totalRoll 52 | } 53 | -------------------------------------------------------------------------------- /data/testdata/enemies_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "level_2": { 3 | "small_rat_2": { 4 | "components": { 5 | "position": {}, 6 | "appearance": { 7 | "Name": "Small rat", 8 | "Description": "A very small rat. It doesn't look very tough.", 9 | "Glyph": { 10 | "Char": "r", 11 | "Color": "brown" 12 | }, 13 | "Layer": 1 14 | }, 15 | "energy": { 16 | "TotalEnergy": 2, 17 | "CurrentEnergy": 0 18 | }, 19 | "blocking": {}, 20 | "cowardly": {}, 21 | "simpleai": {} 22 | } 23 | }, 24 | "large_rat_2": { 25 | "components": { 26 | "position": {}, 27 | "appearance": { 28 | "Name": "Large rat", 29 | "Description": "A very large rat. It's big...but still doesn't look too tough.", 30 | "Glyph": { 31 | "Char": "R", 32 | "Color": "brown" 33 | }, 34 | "Layer": 1 35 | }, 36 | "energy": { 37 | "TotalEnergy": 2, 38 | "CurrentEnergy": 0 39 | }, 40 | "blocking": {}, 41 | "simpleai": {} 42 | } 43 | }, 44 | "cave_bat_2": { 45 | "components": { 46 | "position": {}, 47 | "appearance": { 48 | "Name": "Cave bat", 49 | "Description": "A small, gray, bat. It's fast, bot not a threat. By itself.", 50 | "Glyph": { 51 | "Char": "b", 52 | "Color": "gray" 53 | }, 54 | "Layer": 1 55 | }, 56 | "energy": { 57 | "TotalEnergy": 1, 58 | "CurrentEnergy": 0 59 | }, 60 | "blocking": {}, 61 | "simpleai": {} 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /gamemap/maptypes/maptypes_test.go: -------------------------------------------------------------------------------- 1 | package maptypes 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/gamemap" 5 | "github.com/gogue-framework/gogue/ui" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | wallGlyph ui.Glyph 12 | floorGlyph ui.Glyph 13 | ) 14 | 15 | func TestGenerateArena(t *testing.T) { 16 | wallGlyph = ui.NewGlyph("#", "white", "gray") 17 | floorGlyph = ui.NewGlyph(".", "white", "gray") 18 | 19 | gameMap := &gamemap.GameMap{Width: 50, Height: 50} 20 | gameMap.InitializeMap() 21 | 22 | GenerateArena(gameMap, wallGlyph, floorGlyph) 23 | 24 | assert.Equal(t, gameMap.Tiles[0][0].Glyph, wallGlyph) 25 | assert.Equal(t, gameMap.Tiles[2][2].Glyph, floorGlyph) 26 | } 27 | 28 | func TestGenerateCavern(t *testing.T) { 29 | wallGlyph = ui.NewGlyph("#", "white", "gray") 30 | floorGlyph = ui.NewGlyph(".", "white", "gray") 31 | 32 | gameMap := &gamemap.GameMap{Width: 50, Height: 50} 33 | gameMap.InitializeMap() 34 | 35 | GenerateCavern(gameMap, wallGlyph, floorGlyph, 5) 36 | 37 | // Kind of hard to test procedural code, so we'll just check some basic parameters to ensure that a map was 38 | // actually generated 39 | assert.Greater(t, len(gameMap.FloorTiles), 0) 40 | 41 | // For a cavern, we seal up all the edges of the map, so when x == 0, or y == 0, or x == width, or y == width, 42 | // there should never be a floor 43 | for x := 0; x < gameMap.Width; x++ { 44 | assert.Equal(t, gameMap.Tiles[x][0].Glyph, wallGlyph) 45 | } 46 | 47 | for y := 0; y < gameMap.Width; y++ { 48 | assert.Equal(t, gameMap.Tiles[0][y].Glyph, wallGlyph) 49 | } 50 | 51 | for x := 0; x < gameMap.Width; x++ { 52 | assert.Equal(t, gameMap.Tiles[x][gameMap.Height-1].Glyph, wallGlyph) 53 | } 54 | 55 | for y := 0; y < gameMap.Width; y++ { 56 | assert.Equal(t, gameMap.Tiles[gameMap.Width-1][y].Glyph, wallGlyph) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gogue - Roguelike toolkit for Go 2 | 3 | ![Go](https://github.com/gogue-framework/gogue/workflows/Go/badge.svg?branch=master) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/gogue-framework/gogue)](https://goreportcard.com/report/github.com/gogue-framework/gogue) 5 | 6 | Gogue aims to create a simple to use toolkit for creating Roguelike games in the Go language. It uses BearLibTerminal for rendering, so that will be required to use the toolkit. 7 | This is by no means a complete toolkit, but its got (and will have) a bunch of things I've found handy for building roguelikes. Hopefully someone else finds them handy as well. 8 | 9 | Development is on-going, so use at your own risk. 10 | 11 | ## Features 12 | 13 | This feature list is incomplete, as various pieces are still in development. 14 | 15 | - Terminal creation and management (using BearlibTerminal) 16 | - Colors 17 | - Easy font management 18 | - Glyph rendering 19 | - Input handling 20 | - Dynamic input registration system 21 | - Lightweight Entity/Component/System implementation 22 | - JSON data loading 23 | - Dynamic entity generation from JSON data 24 | - Map generation 25 | - Scrolling camera 26 | - Field of View (only raycasting at the moment, but more to come) 27 | - UI 28 | - Logging 29 | - Screen Management 30 | - Menu system (primitive) 31 | - Pathfinding (A* for now, but also Djikstra Maps) 32 | - Djikstra Maps implementation (http://www.roguebasin.com/index.php?title=The_Incredible_Power_of_Dijkstra_Maps) 33 | - Single entity maps 34 | - multi-entity maps 35 | - combined maps 36 | - Random number generation 37 | - Uniform, Normal Distribution, Ranges, Weighted choices 38 | - Dice rolls (normal and open ended) 39 | - Random name generation (WIP) 40 | 41 | ... and whatever else I deem useful 42 | 43 | ## Getting Started 44 | 45 | Standard Go package install - `go get github.com/gogue-framework/gogue` 46 | 47 | Or if using Modules, simply include `github.com/gogue-framework/gogue` in your project imports and build. 48 | 49 | ### Prerequisites 50 | 51 | BearLibTerminal is required to use this package. You can find install instructions for various operating systems here: https://github.com/gogue-framework/gogue/wiki 52 | -------------------------------------------------------------------------------- /ui/input.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gogue-framework/gogue/ecs" 6 | ) 7 | 8 | // TerminalInputHandler is a container for all keyHandlers and their respective required args. A key handler is 9 | // a function mapped to a keypress event, and the handlerargs are any arguments that need to be passed to that function. 10 | type TerminalInputHandler struct { 11 | keyHandlers map[int]func(map[string]interface{}, *ecs.Controller) 12 | handlerArgs map[int]map[string]interface{} 13 | } 14 | 15 | // NewTerminalInputHandler creates a new TerminalInputHandler instance. This can be used to register key handlers for 16 | // use within the application 17 | func NewTerminalInputHandler() *TerminalInputHandler { 18 | terminalInputHandler := TerminalInputHandler{} 19 | terminalInputHandler.keyHandlers = make(map[int]func(map[string]interface{}, *ecs.Controller)) 20 | terminalInputHandler.handlerArgs = make(map[int]map[string]interface{}) 21 | return &terminalInputHandler 22 | } 23 | 24 | // RegisterInputHandler registers a new key input handler to the provided key. args can optionally be provided 25 | func (ti *TerminalInputHandler) RegisterInputHandler(key int, handlerFunction func(map[string]interface{}, *ecs.Controller), args map[string]interface{}) { 26 | // First, check to see if this key has already been registered 27 | if _, ok := ti.keyHandlers[key]; ok { 28 | fmt.Printf("The key %v has already been registered in this InputHandler. Aborting.", key) 29 | return 30 | } 31 | 32 | // The key has not already been assigned to a handler 33 | ti.keyHandlers[key] = handlerFunction 34 | 35 | // Now check to see if there are any arguments necessary (or provided) to this handler 36 | if len(args) > 0 { 37 | ti.handlerArgs[key] = args 38 | } 39 | } 40 | 41 | // ProcessInput takes a key, checks for a registered handler, and then runs that handler, with any provided args 42 | func (ti *TerminalInputHandler) ProcessInput(key int, controller *ecs.Controller) { 43 | // Check to see if the pressed key has a handler. If it does not, do nothing. 44 | _, ok := ti.keyHandlers[key] 45 | 46 | // If a key handler has been registered, and the state is not currently a menu, go ahead and process the input 47 | // Otherwise, do nothing. 48 | if ok { 49 | keyHandlerFunction := ti.keyHandlers[key] 50 | keyHandlerArgs := ti.handlerArgs[key] 51 | 52 | keyHandlerFunction(keyHandlerArgs, controller) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /ui/ui_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestNewMessageLog(t *testing.T) { 9 | messageLog := NewMessageLog(100) 10 | 11 | assert.NotNil(t, messageLog) 12 | assert.Equal(t, 0, len(messageLog.messages)) 13 | } 14 | 15 | func TestMessageLog_SendMessage(t *testing.T) { 16 | messageLog := NewMessageLog(10) 17 | 18 | messageLog.SendMessage("first message") 19 | assert.Equal(t, 1, len(messageLog.messages)) 20 | assert.Equal(t, "first message", messageLog.messages[0]) 21 | 22 | // Fill up the message log with nine more messages 23 | for i := 0; i < 9; i++ { 24 | messageLog.SendMessage("test message") 25 | } 26 | 27 | // New messages are pre-pended to the messages list (the order they will be displayed to the user), check that our 28 | // first message is now the last element in the list 29 | assert.Equal(t, "first message", messageLog.messages[9]) 30 | assert.Equal(t, 10, len(messageLog.messages)) 31 | 32 | // Add another message. This will cause the first message to be truncated 33 | messageLog.SendMessage("newest message") 34 | assert.Equal(t, 10, len(messageLog.messages)) 35 | assert.Equal(t, "test message", messageLog.messages[9]) 36 | assert.Equal(t, "newest message", messageLog.messages[0]) 37 | 38 | } 39 | 40 | func TestMessageLog_PrintMessages(t *testing.T) { 41 | messageLog := NewMessageLog(10) 42 | 43 | messageLog.SendMessage("first message") 44 | 45 | for i := 0; i < 4; i++ { 46 | messageLog.SendMessage("test message") 47 | } 48 | 49 | messageLog.SendMessage("newest message") 50 | assert.Equal(t, 6, len(messageLog.messages)) 51 | 52 | printedMessages := messageLog.PrintMessages(0, 0, 0, 0, 5) 53 | assert.Equal(t, 5, len(printedMessages)) 54 | assert.Equal(t, "newest message", printedMessages[4]) 55 | 56 | printedMessages = messageLog.PrintMessages(0, 0, 0, 0, 100) 57 | assert.Equal(t, 6, len(printedMessages)) 58 | assert.Equal(t, "newest message", printedMessages[5]) 59 | assert.Equal(t, "first message", printedMessages[0]) 60 | } 61 | 62 | func TestSplitLines(t *testing.T) { 63 | testString := "The quick brown fox jumped over the lazy dog. The lazy dog was actually not that lazy, and chased the fox." 64 | 65 | lines := SplitLines(testString, 40) 66 | assert.Equal(t, 3, len(lines)) 67 | assert.Equal(t, "The quick brown fox jumped over the", lines[0]) 68 | assert.Equal(t, "lazy dog. The lazy dog was actually not", lines[1]) 69 | assert.Equal(t, "that lazy, and chased the fox.", lines[2]) 70 | } 71 | -------------------------------------------------------------------------------- /noises/noiseGeneration.go: -------------------------------------------------------------------------------- 1 | package noises 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/gamemap" 5 | "math" 6 | ) 7 | 8 | // NoiseGenerator generates noises on tiles in a circle around an entity 9 | type NoiseGenerator struct { 10 | cosTable map[int]float64 11 | sinTable map[int]float64 12 | } 13 | 14 | // InitializeNoiseGenerator creates and stores cos and sin tables for 360 degrees, so they don't have to be regenerated 15 | // each time this object is used. 16 | func (f *NoiseGenerator) InitializeNoiseGenerator() { 17 | 18 | f.cosTable = make(map[int]float64) 19 | f.sinTable = make(map[int]float64) 20 | 21 | for i := 0; i < 360; i++ { 22 | ax := math.Sin(float64(i) / (float64(180) / math.Pi)) 23 | ay := math.Cos(float64(i) / (float64(180) / math.Pi)) 24 | 25 | f.sinTable[i] = ax 26 | f.cosTable[i] = ay 27 | } 28 | } 29 | 30 | // RayCastSound casts out rays each degree in a 360 circle from the entity. If a ray passes over a floor (does not block sound) 31 | // tile, keep going, up to the maximum distance the sound can travel from the entity. If the ray intersects a wall 32 | // (blocks sound), stop, as the sound will not penetrate the wall. Every tile that the sound carries through will 33 | // get a noise value corresponding to the entity, and the value of the sound. Sound degrades the further from the 34 | // source it is. 35 | func (f *NoiseGenerator) RayCastSound(entity, entityX, entityY int, intensity float64, gameMap *gamemap.GameMap) { 36 | 37 | for i := 0; i < 360; i++ { 38 | 39 | ax := f.sinTable[i] 40 | ay := f.cosTable[i] 41 | 42 | x := float64(entityX) 43 | y := float64(entityY) 44 | 45 | // Mark the entities current position as the source of the noise. This tile will have the full noise intensity 46 | // value for this frame 47 | tile := gameMap.Tiles[entityX][entityY] 48 | tile.Noises[entity] = intensity 49 | 50 | // Reduce the intensity by a value of 1, and then start raycasting. For each tile away from the source (the 51 | // entities location), reduce the intensity by 1. Once the intensity is 0, stop. 52 | reducedIntensity := intensity - 1 53 | 54 | for j := reducedIntensity; j > 0; j-- { 55 | x -= ax 56 | y -= ay 57 | 58 | roundedX := int(round(x)) 59 | roundedY := int(round(y)) 60 | 61 | if x < 0 || x > float64(gameMap.Width-1) || y < 0 || y > float64(gameMap.Height-1) { 62 | // If the ray is cast outside of the map, stop 63 | break 64 | } 65 | 66 | tile := gameMap.Tiles[roundedX][roundedY] 67 | tile.Noises[entity] = j 68 | 69 | if gameMap.Tiles[roundedX][roundedY].BlocksNoises == true { 70 | // The ray hit a tile that does not transmit sound, go no further 71 | break 72 | } 73 | } 74 | } 75 | } 76 | 77 | func round(f float64) float64 { 78 | return math.Floor(f + .5) 79 | } 80 | -------------------------------------------------------------------------------- /ui/messageLog.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | // MessageLog keeps track of a list messages, and defines how many messages to keep track of before truncating the list 4 | type MessageLog struct { 5 | messages []string 6 | MaxLength int 7 | } 8 | 9 | // NewMessageLog creates a new MessageLog with a maxLength 10 | func NewMessageLog(maxLength int) *MessageLog { 11 | messageLog := MessageLog{MaxLength: maxLength} 12 | messageLog.messages = []string{} 13 | return &messageLog 14 | } 15 | 16 | // SendMessage adds a new message to the MessageLog. If the new message would exceed the total number of messages this 17 | // MessageLog can hold, the oldest message will be truncated from the log. New messages are pre-pended onto the messages 18 | // slice 19 | func (ml *MessageLog) SendMessage(message string) { 20 | // Prepend the message onto the messageLog slice 21 | if len(ml.messages) >= ml.MaxLength { 22 | // Throw away any messages that exceed our total queue size 23 | ml.messages = ml.messages[:len(ml.messages)-1] 24 | } 25 | ml.messages = append([]string{message}, ml.messages...) 26 | } 27 | 28 | // PrintMessages prints messages, up to displayNum, in reverse order (newest messages get printed first). Any messges 29 | // in the messages slice will not be printed 30 | func (ml *MessageLog) PrintMessages(viewAreaX, viewAreaY, windowSizeX, windowSizeY, displayNum int) []string { 31 | // Print the latest five messages from the messageLog. These will be printed in reverse order (newest at the top), 32 | // to make it appear they are scrolling down the screen 33 | clearMessages(viewAreaX, viewAreaY, windowSizeX, windowSizeY, 1) 34 | 35 | toShow := 0 36 | 37 | if len(ml.messages) <= displayNum { 38 | // Just loop through the messageLog, printing them in reverse order 39 | toShow = len(ml.messages) 40 | } else { 41 | // If we have more than {displayNum} messages stored, just show the {displayNum} most recent 42 | toShow = displayNum 43 | } 44 | 45 | printedMessages := []string{} 46 | 47 | for i := toShow; i > 0; i-- { 48 | PrintText(viewAreaX, (viewAreaY-1)+i, ml.messages[i-1], "white", "", 1, 0) 49 | printedMessages = append(printedMessages, ml.messages[i-1]) 50 | } 51 | 52 | return printedMessages 53 | } 54 | 55 | // ClearMessage clears the defined message area, starting at viewAreaX and Y, and ending at the width and height of 56 | // the message area 57 | func clearMessages(viewAreaX, viewAreaY, windowSizeX, windowSizeY, layer int) { 58 | ClearArea(viewAreaX, viewAreaY, windowSizeX, windowSizeY-viewAreaY, 1) 59 | } 60 | 61 | // PrintToMessageArea clears the message area, and print a single message at the top 62 | func PrintToMessageArea(message string, viewAreaX, viewAreaY, windowSizeX, windowSizeY, layer int) { 63 | clearMessages(viewAreaX, viewAreaY, windowSizeX, windowSizeY, layer) 64 | PrintText(1, viewAreaY, message, "white", "", 1, 0) 65 | } 66 | -------------------------------------------------------------------------------- /fov/fov.go: -------------------------------------------------------------------------------- 1 | package fov 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/gamemap" 5 | "math" 6 | ) 7 | 8 | // FieldOfVision represents an area that an entity can see, defined by the torch radius. The cos and sin tables are 9 | // generated once on instantiation, so we don't have to build them each time we want to calculate visible distances. 10 | type FieldOfVision struct { 11 | cosTable map[int]float64 12 | sinTable map[int]float64 13 | torchRadius int 14 | } 15 | 16 | // InitializeFOV generates the cos and sin tables, for 360 degrees, for use when raycasting to determine line of sight 17 | func (f *FieldOfVision) InitializeFOV() { 18 | 19 | f.cosTable = make(map[int]float64) 20 | f.sinTable = make(map[int]float64) 21 | 22 | for i := 0; i < 360; i++ { 23 | ax := math.Sin(float64(i) / (float64(180) / math.Pi)) 24 | ay := math.Cos(float64(i) / (float64(180) / math.Pi)) 25 | 26 | f.sinTable[i] = ax 27 | f.cosTable[i] = ay 28 | } 29 | } 30 | 31 | // SetTorchRadius sets the radius of the FOVs torch, or how far the entity can see 32 | func (f *FieldOfVision) SetTorchRadius(radius int) { 33 | if radius > 1 { 34 | f.torchRadius = radius 35 | } 36 | } 37 | 38 | // SetAllInvisible makes all tiles on the gamemap invisible to the player. 39 | func (f *FieldOfVision) SetAllInvisible(gameMap *gamemap.GameMap) { 40 | for x := 0; x < gameMap.Width; x++ { 41 | for y := 0; y < gameMap.Height; y++ { 42 | gameMap.Tiles[x][y].Visible = false 43 | } 44 | } 45 | } 46 | 47 | // RayCast casts out rays each degree in a 360 circle from the player. If a ray passes over a floor (does not block sight) 48 | // tile, keep going, up to the maximum torch radius (view radius) of the player. If the ray intersects a wall 49 | // (blocks sight), stop, as the player will not be able to see past that. Every visible tile will get the Visible 50 | // and Explored properties set to true. 51 | func (f *FieldOfVision) RayCast(playerX, playerY int, gameMap *gamemap.GameMap) { 52 | 53 | for i := 0; i < 360; i++ { 54 | 55 | ax := f.sinTable[i] 56 | ay := f.cosTable[i] 57 | 58 | x := float64(playerX) 59 | y := float64(playerY) 60 | 61 | // Mark the players current position as explored 62 | tile := gameMap.Tiles[playerX][playerY] 63 | tile.Explored = true 64 | tile.Visible = true 65 | 66 | for j := 0; j < f.torchRadius; j++ { 67 | x -= ax 68 | y -= ay 69 | 70 | roundedX := int(round(x)) 71 | roundedY := int(round(y)) 72 | 73 | if x < 0 || x > float64(gameMap.Width-1) || y < 0 || y > float64(gameMap.Height-1) { 74 | // If the ray is cast outside of the map, stop 75 | break 76 | } 77 | 78 | tile := gameMap.Tiles[roundedX][roundedY] 79 | 80 | tile.Explored = true 81 | tile.Visible = true 82 | 83 | if gameMap.Tiles[roundedX][roundedY].BlocksSight == true { 84 | // The ray hit a wall, go no further 85 | break 86 | } 87 | } 88 | } 89 | } 90 | 91 | func round(f float64) float64 { 92 | return math.Floor(f + .5) 93 | } 94 | -------------------------------------------------------------------------------- /data/dataLoader.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // This will be responsible for taking data files, parsing them, and storing the parsed data in an in memory mapping. 14 | // Data files will contain information such as an item identifier, name, description, and systems that need to be 15 | // attached to the entity, color, stats, etc. 16 | 17 | // FileLoader contains the location where data will be loaded from.. It also allows for loading data from a single file 18 | // or loading data from all files found in the source location. 19 | type FileLoader struct { 20 | dataFilesLocation string 21 | } 22 | 23 | // NewFileLoader creates a new FileLoader. If the location provided is invalid (doesn't exist), an error is returned 24 | func NewFileLoader(dataDir string) (*FileLoader, error) { 25 | fileLoader := FileLoader{} 26 | 27 | // Check if the directory exists. If not, raise an error 28 | if _, err := os.Stat(dataDir); err == nil { 29 | fileLoader.dataFilesLocation = dataDir 30 | } else if os.IsNotExist(err) { 31 | return nil, err 32 | } 33 | 34 | return &fileLoader, nil 35 | } 36 | 37 | // LoadDataFromFile takes a single filename (located in DataLoader.dataFilesLocation), and parses it, returning a 38 | // map representation of the data contained in the file 39 | func (fl *FileLoader) LoadDataFromFile(fileName string) (map[string]interface{}, error) { 40 | filePath := filepath.FromSlash(fl.dataFilesLocation + "/" + fileName) 41 | jsonFile, err := os.Open(filePath) 42 | 43 | if err != nil { 44 | fmt.Println(err) 45 | return nil, err 46 | } 47 | 48 | defer jsonFile.Close() 49 | 50 | byteValue, _ := ioutil.ReadAll(jsonFile) 51 | 52 | var result map[string]interface{} 53 | 54 | json.Unmarshal([]byte(byteValue), &result) 55 | 56 | return result, nil 57 | } 58 | 59 | // LoadAllFromFiles will walk the data directory provided to the FileLoader, and load into dictionaries any data it 60 | // finds, and return these as a map, whose keys are the filenames, and the values the data loaded from those files. 61 | func (fl *FileLoader) LoadAllFromFiles() (map[string]map[string]interface{}, error) { 62 | data := make(map[string]map[string]interface{}) 63 | 64 | var files []string 65 | 66 | err := filepath.Walk(fl.dataFilesLocation, func(path string, info os.FileInfo, err error) error { 67 | files = append(files, path) 68 | return nil 69 | }) 70 | 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | baseDir := fl.dataFilesLocation + "/" 76 | 77 | for _, file := range files { 78 | 79 | if file == fl.dataFilesLocation { 80 | continue 81 | } 82 | 83 | pathElements := strings.Split(file, baseDir) 84 | loadedData, err := fl.LoadDataFromFile(strings.Join(pathElements, "")) 85 | 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | fileName := strings.TrimSuffix(file, path.Ext(file)) 91 | data[fileName] = loadedData 92 | } 93 | 94 | return data, nil 95 | } 96 | -------------------------------------------------------------------------------- /randomnumbergenerator/rng.go: -------------------------------------------------------------------------------- 1 | package randomnumbergenerator 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // RNG is a random number generator. It contains a seed, and the Go standardlib random number generator object. It is 10 | // to generate a variety of random values. 11 | type RNG struct { 12 | seed int64 13 | rand *rand.Rand 14 | } 15 | 16 | // NewRNG creates a new RNG. A seed is set to the current Unix timestamp. 17 | func NewRNG() *RNG { 18 | rng := RNG{} 19 | 20 | // Set the seed to the current time. This can be updated later by the user. 21 | rng.seed = time.Now().UTC().UnixNano() 22 | rng.rand = rand.New(rand.NewSource(rng.seed)) 23 | 24 | return &rng 25 | } 26 | 27 | // GetSeed returns the seed value for the RNG 28 | func (rng *RNG) GetSeed() int64 { 29 | return rng.seed 30 | } 31 | 32 | // SetSeed sets the seed value for the RNG 33 | func (rng *RNG) SetSeed(seed int64) { 34 | rng.seed = seed 35 | } 36 | 37 | // Uniform returns a uniform random value in the range [0.0, 1.0] 38 | func (rng *RNG) Uniform() float64 { 39 | return rng.rand.Float64() 40 | } 41 | 42 | // UniformRange returns a uniform random value across the defined range 43 | func (rng *RNG) UniformRange(a, b float64) float64 { 44 | return a + rng.Uniform()*(b-a) 45 | } 46 | 47 | // Normal returns a random value from a normal (Gaussian) distribution 48 | func (rng *RNG) Normal(mean, stddev float64) float64 { 49 | 50 | var r, x float64 51 | 52 | for r >= 1 || r == 0 { 53 | x = rng.UniformRange(-1.0, 1.0) 54 | y := rng.UniformRange(-1.0, 1.0) 55 | r = x*x + y*y 56 | } 57 | 58 | result := x * math.Sqrt(-2*math.Log(r)/r) 59 | 60 | return mean + stddev*result 61 | 62 | } 63 | 64 | // Percentage returns a value in the range [0, 100] 65 | func (rng *RNG) Percentage() int { 66 | return rng.rand.Intn(100) 67 | } 68 | 69 | // Range returns a value in the defined range 70 | func (rng *RNG) Range(min, max int) int { 71 | if min == max { 72 | return min 73 | } 74 | 75 | return rng.rand.Intn(max-min) + min 76 | } 77 | 78 | // RangeNegative returns a value in the defined range, but allows negative values 79 | func (rng *RNG) RangeNegative(min, max int) int { 80 | if min == max { 81 | return min 82 | } 83 | 84 | return rng.rand.Intn(max-min+1) + min 85 | } 86 | 87 | // GetWeightedEntity takes a weight map (a map of entity IDs with a weight associated with them). It then selects an 88 | // entity based on the weights. An entity with a higher weight is more likely to be chosen over a lower weight entity. 89 | func (rng *RNG) GetWeightedEntity(values map[int]int) int { 90 | // First up, get the total weight value from the map 91 | totalWeight := 0 92 | for weight := range values { 93 | totalWeight += weight 94 | } 95 | 96 | // Next, get a random integer in the range of the total weight 97 | r := rng.Range(0, totalWeight) 98 | 99 | for weight, value := range values { 100 | r -= value 101 | if r <= 0 { 102 | return weight 103 | } 104 | } 105 | 106 | return -1 107 | } 108 | -------------------------------------------------------------------------------- /camera/camera_test.go: -------------------------------------------------------------------------------- 1 | package camera 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestNewCamera(t *testing.T) { 9 | gameCamera, err := NewGameCamera(0, 1, 2, 3) 10 | 11 | assert.NotNil(t, gameCamera) 12 | assert.Nil(t, err) 13 | assert.Equal(t, gameCamera.X, 0) 14 | assert.Equal(t, gameCamera.Y, 1) 15 | assert.Equal(t, gameCamera.Width, 2) 16 | assert.Equal(t, gameCamera.Height, 3) 17 | 18 | // Check to ensure values < 0 are not allowed 19 | gameCamera, err = NewGameCamera(-1, 0, 1, 2) 20 | 21 | assert.NotNil(t, err) 22 | assert.Nil(t, gameCamera) 23 | } 24 | 25 | func TestGameCamera_MoveCamera(t *testing.T) { 26 | // Set up a new camera, with a viewport of 100x100 27 | gameCamera, _ := NewGameCamera(0, 0, 100, 100) 28 | 29 | // Move the camera to a location that is already within the cameras viewport. This effectively will not 30 | // move the viewport 31 | gameCamera.MoveCamera(25, 25, 100, 100) 32 | assert.Equal(t, gameCamera.X, 0) 33 | assert.Equal(t, gameCamera.Y, 0) 34 | 35 | // Move the camera to a location that is more than halfway across the map, and the cameras viewport, on a small 36 | // map. This should not move the camera, since the entire map is within the cameras viewport 37 | gameCamera.MoveCamera(51, 51, 100, 100) 38 | assert.Equal(t, gameCamera.X, 0) 39 | assert.Equal(t, gameCamera.Y, 0) 40 | 41 | // Move the camera to a location that is outside of its viewport (on a larger map). This should update the 42 | // the cameras coordinates 43 | gameCamera.MoveCamera(101, 101, 500, 500) 44 | assert.Equal(t, gameCamera.X, 51) 45 | assert.Equal(t, gameCamera.Y, 51) 46 | } 47 | 48 | func TestGameCamera_ToCameraCoordinates(t *testing.T) { 49 | // GameMap coordinates and Camera coordinates are two different things. A GameMap coordinate pair designates a 50 | // a location on the GameMap, whereas a Camera coordinate pair designates a location within the cameras viewport. 51 | // ToCameraCoordinates translates a GameMap coordinate pair to a Camera coordinate pair. If the map coordinate pair 52 | // is outside of the viewport of the camera, (-1, -1) is returned to indicate this 53 | gameCamera, _ := NewGameCamera(0, 0, 100, 100) 54 | 55 | // With the camera viewport matching the top left corner of the map, the coordinates should be the same for both 56 | cameraX, cameraY := gameCamera.ToCameraCoordinates(2, 2) 57 | assert.Equal(t, cameraX, 2) 58 | assert.Equal(t, cameraY, 2) 59 | 60 | // If the camera is moved beyond the top left corner of the map, the two coordinate systems no longer align 61 | gameCamera.MoveCamera(50, 70, 500, 500) 62 | cameraX, cameraY = gameCamera.ToCameraCoordinates(40, 50) 63 | assert.Equal(t, cameraX, 40) 64 | assert.Equal(t, cameraY, 30) 65 | 66 | // If a map coordinate pair that is outside of the viewport of the camera is provided, (-1, -1) should be returned 67 | cameraX, cameraY = gameCamera.ToCameraCoordinates(1, 1) 68 | assert.Equal(t, cameraX, -1) 69 | assert.Equal(t, cameraY, -1) 70 | 71 | cameraX, cameraY = gameCamera.ToCameraCoordinates(499, 499) 72 | assert.Equal(t, cameraX, -1) 73 | assert.Equal(t, cameraY, -1) 74 | } 75 | -------------------------------------------------------------------------------- /camera/camera.go: -------------------------------------------------------------------------------- 1 | package camera 2 | 3 | import "errors" 4 | 5 | // GameCamera represents a viewport over the game map. It is a rectangle, with origin at (X, Y), and a width and height 6 | // specified. Only content within this viewport will be shown to the user, nothing outside the bounds will be drawn. 7 | type GameCamera struct { 8 | X int 9 | Y int 10 | Width int 11 | Height int 12 | } 13 | 14 | // NewGameCamera creates a new GameCamera instance. (x, y) are the starting location for the camera viewport. The 15 | // viewport will be centered over these coordinates on the GameMap. width and height designate the size of the 16 | // cameras viewport, or how much of the GameMap is shown. If the viewport is the same size as, or larger than the 17 | // GameMap, the entire map will be shown. If the viewport is smaller than the GameMap, only the portion of the GameMap 18 | // that fits within the centered viewport will be shown. 19 | func NewGameCamera(x, y, width, height int) (*GameCamera, error) { 20 | gameCamera := GameCamera{} 21 | 22 | if x < 0 || y < 0 || width < 0 || height < 0 { 23 | err := errors.New("GameCamera position values must be greater than 0") 24 | return nil, err 25 | } 26 | 27 | gameCamera.X = x 28 | gameCamera.Y = y 29 | gameCamera.Width = width 30 | gameCamera.Height = height 31 | 32 | return &gameCamera, nil 33 | } 34 | 35 | // MoveCamera centers the GameCamera viewport to the new location specified over the GameMap. If the viewport is 36 | // larger than the GameMap, the viewport is not moved. If the viewport is the same size as the GameMap, the viewport 37 | // is not moved. If the GameMap is larger than the viewport, and coordinates outside of the viewport are requested, 38 | // the viewport is centered over the new coordinate set. The viewport cannot be centered outside the bounds of the 39 | // GameMap. 40 | func (c *GameCamera) MoveCamera(targetX int, targetY int, mapWidth int, mapHeight int) { 41 | // Update the camera coordinates to the target coordinates 42 | x := targetX - c.Width/2 43 | y := targetY - c.Height/2 44 | 45 | if x < 0 { 46 | x = 0 47 | } 48 | 49 | if y < 0 { 50 | y = 0 51 | } 52 | 53 | if x > mapWidth-c.Width { 54 | x = mapWidth - c.Width 55 | } 56 | 57 | if y > mapHeight-c.Height { 58 | y = mapHeight - c.Height 59 | } 60 | 61 | c.X, c.Y = x, y 62 | } 63 | 64 | // ToCameraCoordinates translates a GameMap coordinate pair to a GameCamera coordinate pair. GameMap coordinates and 65 | // Camera coordinates are two different things. A GameMap coordinate pair designates a location on the GameMap, whereas 66 | // a Camera coordinate pair designates a location within the cameras viewport. ToCameraCoordinates translates a GameMap 67 | // coordinate pair to a Camera coordinate pair. If the map coordinate pair is outside of the viewport of the camera, 68 | // (-1, -1) is returned to indicate this 69 | func (c *GameCamera) ToCameraCoordinates(mapX int, mapY int) (cameraX int, cameraY int) { 70 | // Convert coordinates on the map, to coordinates on the viewport 71 | x, y := mapX-c.X, mapY-c.Y 72 | 73 | if x < 0 || y < 0 || x >= c.Width || y >= c.Height { 74 | return -1, -1 75 | } 76 | 77 | return x, y 78 | } 79 | -------------------------------------------------------------------------------- /gamemap/gamemap_test.go: -------------------------------------------------------------------------------- 1 | package gamemap 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/ui" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | // generateArena takes a GameMap object, and creates a giant room, ringed with walls. This is a very simple type of 10 | // map that contains no features other than the walls. 11 | func generateArena(surface *GameMap, wallGlyph, floorGlyph ui.Glyph) { 12 | // Generates a large, empty room, with walls ringing the outside edges 13 | for x := 0; x <= surface.Width; x++ { 14 | for y := 0; y <= surface.Height; y++ { 15 | if x == 0 || x == surface.Width-1 || y == 0 || y == surface.Height-1 { 16 | surface.Tiles[x][y] = &Tile{Glyph: wallGlyph, Blocked: true, BlocksSight: true, Visited: false, Explored: false, Visible: false, X: x, Y: y} 17 | } else { 18 | surface.Tiles[x][y] = &Tile{Glyph: floorGlyph, Blocked: false, BlocksSight: false, Visited: false, Explored: false, Visible: false, X: x, Y: y} 19 | 20 | // Add the tile to the list of floor tiles that have been created. This will be used to add items, 21 | // monsters, the player, etc 22 | surface.FloorTiles = append(surface.FloorTiles, surface.Tiles[x][y]) 23 | } 24 | } 25 | } 26 | } 27 | 28 | func TestTile_IsWall(t *testing.T) { 29 | glyph := ui.NewGlyph("#", "white", "white") 30 | noises := make(map[int]float64) 31 | tile := Tile{glyph, true, true, true, false, false, false, 1, 1, noises} 32 | 33 | assert.True(t, tile.IsWall()) 34 | 35 | tile.Blocked = false 36 | assert.False(t, tile.IsWall()) 37 | } 38 | 39 | func TestMap_InitializeMap(t *testing.T) { 40 | gameMap := GameMap{Width: 100, Height: 100} 41 | 42 | gameMap.InitializeMap() 43 | 44 | assert.Equal(t, len(gameMap.Tiles), 101) 45 | assert.Equal(t, len(gameMap.Tiles[0]), 101) 46 | } 47 | 48 | func TestMap_IsBlocked(t *testing.T) { 49 | wallGlyph := ui.NewGlyph("#", "white", "gray") 50 | floorGlyph := ui.NewGlyph(".", "white", "gray") 51 | 52 | gameMap := GameMap{Width: 100, Height: 100} 53 | gameMap.InitializeMap() 54 | 55 | // Generate an arena style map 56 | generateArena(&gameMap, wallGlyph, floorGlyph) 57 | 58 | // If map generation went correctly, the Tile at position (0, 0) should be a wall 59 | topLeftCornerWall := gameMap.Tiles[0][0] 60 | assert.True(t, topLeftCornerWall.IsWall()) 61 | assert.True(t, gameMap.IsBlocked(0, 0)) 62 | 63 | floorTile := gameMap.Tiles[1][1] 64 | assert.False(t, floorTile.IsWall()) 65 | assert.False(t, gameMap.IsBlocked(1, 1)) 66 | } 67 | 68 | func TestMap_GetNeighbors(t *testing.T) { 69 | wallGlyph := ui.NewGlyph("#", "white", "gray") 70 | floorGlyph := ui.NewGlyph(".", "white", "gray") 71 | 72 | gameMap := GameMap{Width: 100, Height: 100} 73 | gameMap.InitializeMap() 74 | 75 | // Generate an arena style map 76 | generateArena(&gameMap, wallGlyph, floorGlyph) 77 | 78 | // Get the neighbors of the Tile at (1, 1). This should return eight Tiles, at [(0, 0), (0, 1), (0, 2), (1, 2), 79 | // (2, 2), (2, 1), (2, 0), (1, 0)] 80 | neighbors := gameMap.GetNeighbors(1, 1) 81 | assert.Equal(t, 8, len(neighbors)) 82 | 83 | // Get the neighbors of the Tile at (0, 0). This should return three Tiles, at [(0, 1), (1, 1), (1, 0)] 84 | neighbors = gameMap.GetNeighbors(0, 0) 85 | assert.Equal(t, 3, len(neighbors)) 86 | 87 | // Ensure that edge cases on the opposite end work as well 88 | neighbors = gameMap.GetNeighbors(99, 99) 89 | assert.Equal(t, 8, len(neighbors)) 90 | 91 | neighbors = gameMap.GetNeighbors(100, 100) 92 | assert.Equal(t, 3, len(neighbors)) 93 | 94 | // And make sure a random value is also correct 95 | neighbors = gameMap.GetNeighbors(10, 37) 96 | assert.Equal(t, 8, len(neighbors)) 97 | } 98 | -------------------------------------------------------------------------------- /screens/screen.go: -------------------------------------------------------------------------------- 1 | package screens 2 | 3 | import "fmt" 4 | 5 | // Screen is display state for the game. It has hooks for when it is entered, exited, and rendered. A Screen can have 6 | // inputs associated with it, and it can optionally use the ECS if that is in use. If the ECS is used for a screen, 7 | // all systems will be processed on each frame while the user is interacting with the screen. 8 | type Screen interface { 9 | Enter() 10 | Exit() 11 | Render() 12 | HandleInput() 13 | UseEcs() bool 14 | } 15 | 16 | // ScreenManager is a coordinator for all defined screens. This is meant as a singleton, and keeps track of all screens, 17 | // as well as the previous and current screens, to facilitate easy switching between Screens. 18 | type ScreenManager struct { 19 | Screens map[string]Screen 20 | CurrentScreen Screen 21 | PreviousScreen Screen 22 | } 23 | 24 | // NewScreenManager is a convenience/constructor method to properly initialize a new ScreenManager 25 | func NewScreenManager() *ScreenManager { 26 | manager := ScreenManager{} 27 | manager.Screens = make(map[string]Screen) 28 | manager.CurrentScreen = nil 29 | 30 | return &manager 31 | } 32 | 33 | // AddScreen adds a Screen to a ScreenManager. This checks to see if a Screen with the given screenname already exists 34 | // on the ScreenManager, and returns an error if so. Otherwise, the screen is added to the ScreenManager under the given 35 | // name 36 | func (sm *ScreenManager) AddScreen(screenName string, screen Screen) error { 37 | // Check to see if a screen with the given screenName has already been added 38 | if _, ok := sm.Screens[screenName]; !ok { 39 | // A screen with the given name does not yet exist on the ScreenManager, go ahead and add it 40 | sm.Screens[screenName] = screen 41 | return nil 42 | } 43 | 44 | err := fmt.Errorf("screen with name %v was already added to the ScreenManager %v", screenName, sm) 45 | return err 46 | } 47 | 48 | // RemoveScreen will remove a screen from the ScreenManager. This can be useful when a temporary screen needs to be 49 | // created, as it can be quickly added (rather than registering at game creation), and then removed when it is no 50 | // longer needed 51 | func (sm *ScreenManager) RemoveScreen(screenName string) error { 52 | // Check if the given screenName exists in the ScreenManager 53 | if _, ok := sm.Screens[screenName]; ok { 54 | delete(sm.Screens, screenName) 55 | return nil 56 | } 57 | 58 | // A screen with the given name does not exist 59 | err := fmt.Errorf("screen with name %v was not found on ScreenManager %v", screenName, sm) 60 | return err 61 | } 62 | 63 | // SetScreen will set the current screen property of the screen manager to the provided screen 64 | func (sm *ScreenManager) setScreen(screen Screen) { 65 | // Call the exit function of the currentScreen, and set the currentScreen as the previousScreen 66 | // Only do this if there is a currentScreen 67 | if sm.CurrentScreen != nil { 68 | sm.CurrentScreen.Exit() 69 | sm.PreviousScreen = sm.CurrentScreen 70 | } 71 | 72 | // Set the provided screen as the currentScreen, and call the enter() function of the new currentScreen 73 | sm.CurrentScreen = screen 74 | sm.CurrentScreen.Enter() 75 | } 76 | 77 | // SetScreenByName takes a string representing the screen desired to navigate to. It will then transition the 78 | // ScreenManager to the specified screen, if one is found. 79 | func (sm *ScreenManager) SetScreenByName(screenName string) error { 80 | // Check if the given screenName exists in the ScreenManager 81 | if _, ok := sm.Screens[screenName]; ok { 82 | sm.setScreen(sm.Screens[screenName]) 83 | return nil 84 | } 85 | 86 | // A screen with the given name does not exist 87 | err := fmt.Errorf("screen with name %v was not found on ScreenManager %v", screenName, sm) 88 | return err 89 | } 90 | -------------------------------------------------------------------------------- /ecs/systemsMessages.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | // SystemMessageType represents a type of SystemMessage 4 | type SystemMessageType struct { 5 | Name string 6 | } 7 | 8 | // SystemMessage is a message sent between ECS systems. It contains a type, an origin, and content. A system can send 9 | // a message of a type, and any subscribers to that type will be notified that the message was sent, and can then act 10 | // accordingly. 11 | type SystemMessage struct { 12 | MessageType SystemMessageType 13 | Originator System 14 | MessageContent map[string]string 15 | } 16 | 17 | // SystemMessageQueue is a super simple way of messaging between systems. Essentially, it is nothing more than a list of 18 | // messages. Each message has a type, and an originator. Each system can "subscribe" to a type of message, which 19 | // basically just means that it will check the queue for any messages of that type before it does anything else. 20 | // Messages can contain a map of information, which each system that creates messages of that type, and those that 21 | // subscribe to it should know how to handle any information contained in the message. Ideally, the message queue will 22 | // be cleared out occasionally, either by the subscribing systems, or the game loop. Pretty simple for now, but should 23 | // solve a subset of problems nicely. 24 | type SystemMessageQueue struct { 25 | Messages map[System][]SystemMessage 26 | Subscriptions map[System][]SystemMessageType 27 | } 28 | 29 | // NewSystemMessageQueue creates and initializes a new SystemMessageQueue, used for passing messages between ECS Systems 30 | func NewSystemMessageQueue() *SystemMessageQueue { 31 | smq := SystemMessageQueue{} 32 | smq.Messages = make(map[System][]SystemMessage) 33 | smq.Subscriptions = make(map[System][]SystemMessageType) 34 | return &smq 35 | } 36 | 37 | // BroadcastMessage appends a system message onto the games SystemMessageQueue, allowing it to consumed by a service 38 | // subscribes to the MessageType. 39 | func (smq *SystemMessageQueue) BroadcastMessage(messageType SystemMessageType, messageContent map[string]string, originator System) { 40 | newMessage := SystemMessage{MessageType: messageType, MessageContent: messageContent, Originator: originator} 41 | 42 | // Find all subscriptions to this message type, and add this message to the subscribers message queue 43 | for subscribedSystem, typeList := range smq.Subscriptions { 44 | if MessageTypeInSlice(messageType, typeList) { 45 | smq.Messages[subscribedSystem] = append(smq.Messages[subscribedSystem], newMessage) 46 | } 47 | } 48 | } 49 | 50 | // GetSubscribedMessages returns a list of SystemMessages that have messageType. Can return an empty list 51 | func (smq *SystemMessageQueue) GetSubscribedMessages(system System) []SystemMessage { 52 | messages := []SystemMessage{} 53 | 54 | for _, message := range smq.Messages[system] { 55 | messages = append(messages, message) 56 | } 57 | 58 | return messages 59 | } 60 | 61 | // DeleteMessages deletes a processed message from the queue (for example, if the event has been processed) 62 | func (smq *SystemMessageQueue) DeleteMessages(messageName string, system System) { 63 | modifiedQueue := smq.Messages[system] 64 | for index, message := range smq.Messages[system] { 65 | if message.MessageType.Name == messageName { 66 | modifiedQueue[index] = modifiedQueue[len(modifiedQueue)-1] 67 | modifiedQueue = modifiedQueue[:len(modifiedQueue)-1] 68 | } 69 | } 70 | 71 | smq.Messages[system] = modifiedQueue 72 | } 73 | 74 | //MessageTypeInSlice will return true if the MessageType provided is present in the slice provided, false otherwise 75 | func MessageTypeInSlice(a SystemMessageType, list []SystemMessageType) bool { 76 | for _, b := range list { 77 | if b == a { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | 84 | //MessageTypeInSliceOfMessages will return true if the MessageType provided is present in the slice provided, false otherwise 85 | func MessageTypeInSliceOfMessages(a SystemMessageType, list []SystemMessage) bool { 86 | for _, b := range list { 87 | if b.MessageType == a { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /screens/screens_test.go: -------------------------------------------------------------------------------- 1 | package screens 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | // Create a few screens for testing purposes 9 | type MenuScreen struct { 10 | exitCalled bool 11 | enterCalled bool 12 | renderCalled bool 13 | } 14 | 15 | func (ms *MenuScreen) Enter() { ms.enterCalled = true } 16 | func (ms *MenuScreen) Exit() { ms.exitCalled = true } 17 | func (ms *MenuScreen) Render() { ms.renderCalled = true } 18 | func (ms *MenuScreen) HandleInput() {} 19 | func (ms *MenuScreen) UseEcs() bool { return false } 20 | 21 | type GameScreen struct { 22 | exitCalled bool 23 | enterCalled bool 24 | renderCalled bool 25 | } 26 | 27 | func (gs *GameScreen) Enter() { gs.enterCalled = true } 28 | func (gs *GameScreen) Exit() { gs.exitCalled = true } 29 | func (gs *GameScreen) Render() { gs.renderCalled = true } 30 | func (gs *GameScreen) HandleInput() {} 31 | func (gs *GameScreen) UseEcs() bool { return false } 32 | 33 | func TestNewScreenManager(t *testing.T) { 34 | manager := NewScreenManager() 35 | 36 | assert.Nil(t, manager.CurrentScreen) 37 | assert.Equal(t, 0, len(manager.Screens)) 38 | } 39 | 40 | func TestScreenManager_AddScreen(t *testing.T) { 41 | manager := NewScreenManager() 42 | 43 | menu := &MenuScreen{} 44 | game := &GameScreen{} 45 | 46 | err := manager.AddScreen("menu", menu) 47 | assert.Nil(t, err) 48 | assert.Equal(t, 1, len(manager.Screens)) 49 | 50 | expectedScreensMap := map[string]Screen{ 51 | "menu": menu, 52 | } 53 | assert.Equal(t, expectedScreensMap, manager.Screens) 54 | 55 | err = manager.AddScreen("menu", menu) 56 | assert.NotNil(t, err) 57 | 58 | err = manager.AddScreen("game", game) 59 | expectedScreensMap["game"] = game 60 | 61 | assert.Nil(t, err) 62 | assert.Equal(t, 2, len(manager.Screens)) 63 | assert.Equal(t, expectedScreensMap, manager.Screens) 64 | } 65 | 66 | func TestScreenManager_RemoveScreen(t *testing.T) { 67 | manager := NewScreenManager() 68 | 69 | menu := &MenuScreen{} 70 | game := &GameScreen{} 71 | 72 | _ = manager.AddScreen("menu", menu) 73 | _ = manager.AddScreen("game", game) 74 | 75 | expectedScreensMap := map[string]Screen{ 76 | "menu": menu, 77 | "game": game, 78 | } 79 | 80 | assert.Equal(t, expectedScreensMap, manager.Screens) 81 | 82 | // Now, remove a screen 83 | err := manager.RemoveScreen("game") 84 | delete(expectedScreensMap, "game") 85 | assert.Nil(t, err) 86 | assert.Equal(t, 1, len(manager.Screens)) 87 | assert.Equal(t, expectedScreensMap, manager.Screens) 88 | 89 | // Attempt to remove a screen that is not on the ScreenManager 90 | err = manager.RemoveScreen("does_not_exist") 91 | assert.NotNil(t, err) 92 | assert.Equal(t, 1, len(manager.Screens)) 93 | } 94 | 95 | func TestScreenManager_setScreen(t *testing.T) { 96 | manager := NewScreenManager() 97 | 98 | menu := &MenuScreen{} 99 | game := &GameScreen{} 100 | 101 | _ = manager.AddScreen("menu", menu) 102 | _ = manager.AddScreen("game", game) 103 | 104 | manager.setScreen(menu) 105 | // Since there is no current screen to exit, exit will not be called 106 | assert.False(t, menu.exitCalled) 107 | assert.True(t, menu.enterCalled) 108 | assert.Equal(t, menu, manager.CurrentScreen) 109 | assert.Nil(t, manager.PreviousScreen) 110 | 111 | manager.setScreen(game) 112 | assert.False(t, game.exitCalled) 113 | assert.True(t, menu.exitCalled) 114 | assert.True(t, game.enterCalled) 115 | assert.Equal(t, game, manager.CurrentScreen) 116 | assert.Equal(t, menu, manager.PreviousScreen) 117 | } 118 | 119 | func TestScreenManager_SetScreenByName(t *testing.T) { 120 | manager := NewScreenManager() 121 | 122 | menu := &MenuScreen{} 123 | game := &GameScreen{} 124 | 125 | _ = manager.AddScreen("menu", menu) 126 | _ = manager.AddScreen("game", game) 127 | 128 | err := manager.SetScreenByName("menu") 129 | assert.Nil(t, err) 130 | // Since there is no current screen to exit, exit will not be called 131 | assert.False(t, menu.exitCalled) 132 | assert.True(t, menu.enterCalled) 133 | assert.Equal(t, menu, manager.CurrentScreen) 134 | assert.Nil(t, manager.PreviousScreen) 135 | 136 | err = manager.SetScreenByName("game") 137 | assert.Nil(t, err) 138 | assert.False(t, game.exitCalled) 139 | assert.True(t, menu.exitCalled) 140 | assert.True(t, game.enterCalled) 141 | assert.Equal(t, game, manager.CurrentScreen) 142 | assert.Equal(t, menu, manager.PreviousScreen) 143 | 144 | err = manager.SetScreenByName("nonExistant") 145 | assert.NotNil(t, err) 146 | } 147 | -------------------------------------------------------------------------------- /data/data_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/ecs" 5 | "github.com/gogue-framework/gogue/ui" 6 | "github.com/stretchr/testify/assert" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestNewFileLoader(t *testing.T) { 12 | fileLoader, err := NewFileLoader("testdata") 13 | 14 | assert.Nil(t, err, "NewFileLoader raised an error") 15 | assert.Equal(t, fileLoader.dataFilesLocation, "testdata") 16 | 17 | fileLoader, err = NewFileLoader("does/not/exist") 18 | 19 | assert.NotNil(t, err, "NewFileLoader did not raise an error") 20 | assert.Nil(t, fileLoader) 21 | } 22 | 23 | func TestFileLoader_LoadDataFromFile(t *testing.T) { 24 | fileLoader, _ := NewFileLoader("testdata") 25 | dataFile := "enemies.json" 26 | 27 | dataMap, err := fileLoader.LoadDataFromFile(dataFile) 28 | 29 | assert.Nil(t, err, "err is not Nil") 30 | assert.Equal(t, len(dataMap), 1) 31 | 32 | levelOne := dataMap["level_1"].(map[string]interface{}) 33 | 34 | assert.Equal(t, len(levelOne), 3) 35 | 36 | smallRat := levelOne["small_rat"].(map[string]interface{}) 37 | components := smallRat["components"].(map[string]interface{}) 38 | appearance := components["appearance"].(map[string]interface{}) 39 | 40 | assert.Equal(t, appearance["Name"], "Small rat") 41 | 42 | // Ensure a non-existant file will properly raise an error 43 | dataMap, err = fileLoader.LoadDataFromFile("does_not_exist.json") 44 | 45 | assert.Nil(t, dataMap, "dataMap is not nil") 46 | assert.NotNil(t, err) 47 | } 48 | 49 | func TestFileLoader_LoadAllFromFiles(t *testing.T) { 50 | fileLoader, _ := NewFileLoader("testdata") 51 | 52 | dataMap, err := fileLoader.LoadAllFromFiles() 53 | 54 | assert.Nil(t, err, "err is not Nil") 55 | assert.Equal(t, len(dataMap), 2) 56 | 57 | levelOne := dataMap["testdata/enemies"]["level_1"].(map[string]interface{}) 58 | levelTwo := dataMap["testdata/enemies_2"]["level_2"].(map[string]interface{}) 59 | 60 | assert.Equal(t, len(levelOne), 3) 61 | assert.Equal(t, len(levelTwo), 3) 62 | 63 | } 64 | 65 | func TestNewEntityLoader(t *testing.T) { 66 | controller := ecs.NewController() 67 | entityLoader := NewEntityLoader(controller) 68 | 69 | assert.NotNil(t, entityLoader) 70 | assert.Equal(t, entityLoader.controller, controller) 71 | } 72 | 73 | // Components for EntityLoader tests 74 | type PositionComponent struct { 75 | X int 76 | Y int 77 | } 78 | 79 | func (pc PositionComponent) TypeOf() reflect.Type { 80 | return reflect.TypeOf(pc) 81 | } 82 | 83 | type AppearanceComponent struct { 84 | Name string 85 | Description string 86 | Glyph ui.Glyph 87 | Layer int 88 | } 89 | 90 | func (ac AppearanceComponent) TypeOf() reflect.Type { 91 | return reflect.TypeOf(ac) 92 | } 93 | 94 | func TestEntityLoader_CreateSingleEntity(t *testing.T) { 95 | controller := ecs.NewController() 96 | 97 | // Load a couple of components into the controller 98 | controller.MapComponentClass("position", PositionComponent{}) 99 | controller.MapComponentClass("appearance", AppearanceComponent{}) 100 | 101 | dataLoader, _ := NewFileLoader("testdata") 102 | entityLoader := NewEntityLoader(controller) 103 | 104 | dataMap, _ := dataLoader.LoadDataFromFile("enemies.json") 105 | levelOne := dataMap["level_1"].(map[string]interface{}) 106 | smallRat := levelOne["small_rat"].(map[string]interface{}) 107 | caveBat := levelOne["cave_bat"].(map[string]interface{}) 108 | 109 | entityID := entityLoader.CreateSingleEntity(smallRat) 110 | 111 | assert.Equal(t, entityID, 0) 112 | assert.True(t, controller.HasComponent(entityID, reflect.TypeOf(PositionComponent{}))) 113 | assert.True(t, controller.HasComponent(entityID, reflect.TypeOf(AppearanceComponent{}))) 114 | 115 | appearance := controller.GetComponent(entityID, AppearanceComponent{}.TypeOf()).(AppearanceComponent) 116 | 117 | assert.Equal(t, "Small rat", appearance.Name) 118 | assert.Equal(t, "r", appearance.Glyph.Char()) 119 | assert.Equal(t, "brown", appearance.Glyph.Color()) 120 | 121 | entityID = entityLoader.CreateSingleEntity(caveBat) 122 | 123 | assert.Equal(t, entityID, 1) 124 | assert.True(t, controller.HasComponent(entityID, reflect.TypeOf(PositionComponent{}))) 125 | assert.True(t, controller.HasComponent(entityID, reflect.TypeOf(AppearanceComponent{}))) 126 | 127 | appearance = controller.GetComponent(entityID, AppearanceComponent{}.TypeOf()).(AppearanceComponent) 128 | 129 | assert.Equal(t, "Cave bat", appearance.Name) 130 | assert.Equal(t, "b", appearance.Glyph.Char()) 131 | assert.Equal(t, "gray", appearance.Glyph.Color()) 132 | } 133 | -------------------------------------------------------------------------------- /data/entityLoader.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/ecs" 5 | "github.com/gogue-framework/gogue/ui" 6 | "reflect" 7 | ) 8 | 9 | // EntityLoader responsible for taking loaded data from the text files (in the form of a map of strings), and turning 10 | // those into entities, as required. For example, we may have loaded several potion definitions into memory from the 11 | // definition files, and we now want to use those in the game. In order to do that, we would find the potion we want 12 | // to load, and then take that definition and turn it into an entity in the ECS. We can do this as many times as we 13 | // need per potion definition. In this way, we have an easy way of loading data file information into the ECS. 14 | type EntityLoader struct { 15 | controller *ecs.Controller 16 | } 17 | 18 | // NewEntityLoader creates a new instance of an EntityLoader 19 | func NewEntityLoader(controller *ecs.Controller) *EntityLoader { 20 | entityLoader := EntityLoader{} 21 | entityLoader.controller = controller 22 | 23 | return &entityLoader 24 | } 25 | 26 | // CreateSingleEntity takes a map of generic interface data (returned from Gogues data loader), and creates a single 27 | // instance entity out of it. It will add any indicated components, and any data associated with those components. This 28 | // will return the entity ID. 29 | func (el *EntityLoader) CreateSingleEntity(data map[string]interface{}) int { 30 | // First, check to ensure there is a components property in the map. If this is not present, we cannot continue 31 | if _, ok := data["components"]; ok { 32 | componentList := data["components"].(map[string]interface{}) 33 | 34 | // Create a new entity 35 | newEntity := el.controller.CreateEntity([]ecs.Component{}) 36 | 37 | for componentName, values := range componentList { 38 | // Grab the component type off the controller. Also, ensure that this component type has been registered 39 | // with the controller 40 | component := el.controller.GetMappedComponentClass(componentName) 41 | 42 | if component != nil { 43 | newComponentValue := el.getInterfaceValue(component) 44 | 45 | valuesMap := values.(map[string]interface{}) 46 | el.setFieldValues(valuesMap, newComponentValue) 47 | // Finally, update the new component with the changes we made based on the property values 48 | newComponentInterface := newComponentValue.Interface() 49 | updatedNewComponent := newComponentInterface 50 | 51 | // Add the component to the created entity 52 | el.controller.AddComponent(newEntity, updatedNewComponent.(ecs.Component)) 53 | } 54 | } 55 | 56 | return newEntity 57 | } 58 | return -1 59 | } 60 | 61 | // getInterfaceValue returns the reflect.Value of a generic interface 62 | func (el *EntityLoader) getInterfaceValue(reflectedInterface interface{}) reflect.Value { 63 | iType := reflect.TypeOf(reflectedInterface) 64 | iPointer := reflect.New(iType) 65 | iValue := iPointer.Elem() 66 | iInterface := iValue.Interface() 67 | newInterface := iInterface 68 | newInterfaceType := reflect.TypeOf(newInterface) 69 | newInterfaceValue := reflect.New(newInterfaceType).Elem() 70 | 71 | return newInterfaceValue 72 | } 73 | 74 | // setFieldValues will dynamically set interface property values for a given generic interface reflect.Value. 75 | func (el *EntityLoader) setFieldValues(values map[string]interface{}, value reflect.Value) { 76 | for propertyName, propertyValue := range values { 77 | field := value.FieldByName(propertyName) 78 | 79 | if field.IsValid() && field.CanSet() { 80 | if field.Kind() == reflect.Int64 { 81 | field.SetInt(propertyValue.(int64)) 82 | } else if field.Kind() == reflect.Float64 { 83 | field.SetFloat(propertyValue.(float64)) 84 | } else if field.Kind() == reflect.String { 85 | field.SetString(propertyValue.(string)) 86 | } else if field.Kind() == reflect.Interface { 87 | // There are only a few nested interface properties, so we'll just handle them manually 88 | if propertyName == "Glyph" { 89 | // Glyphs are a little weird. They don't expose any public setters, so we can't dynamically discover 90 | // and set their properties. We have to do it a bit more...manually 91 | glyphValues := propertyValue.(map[string]interface{}) 92 | color := "" 93 | char := "" 94 | for glyphPropName, glyphPropValue := range glyphValues { 95 | if glyphPropName == "Color" { 96 | color = glyphPropValue.(string) 97 | } else if glyphPropName == "Char" { 98 | char = glyphPropValue.(string) 99 | } 100 | } 101 | glyph := ui.NewGlyph(char, color, "") 102 | field.Set(reflect.ValueOf(glyph)) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ui/menu.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "sort" 7 | ) 8 | 9 | const ( 10 | ordLowerStart = 97 11 | maxOrd = 122 12 | ) 13 | 14 | // MenuList is a list menu, with choices presented with a key for each. Options is the list of menu options, Inputs 15 | // specifies which keys map to which options, keys is a list of valid keypresses, Paginated defines if the list should 16 | // be displayed all at once, or in pages, and highestOrd defines teh maximum ASCII keypress available for this menu 17 | type MenuList struct { 18 | Options map[int]string 19 | Inputs map[rune]int 20 | keys []int 21 | Paginated bool 22 | highestOrd int 23 | } 24 | 25 | // NewMenuList creates a new MenuList 26 | func NewMenuList(options map[int]string) *MenuList { 27 | menuList := MenuList{} 28 | menuList.Options = make(map[int]string) 29 | menuList.Inputs = make(map[rune]int) 30 | menuList.highestOrd = ordLowerStart 31 | menuList.Create(options) 32 | 33 | return &menuList 34 | 35 | } 36 | 37 | // Create builds a new MenuList, given a mapping of options 38 | func (ml *MenuList) Create(options map[int]string) { 39 | ml.Options = options 40 | 41 | ordLower := ordLowerStart 42 | 43 | for identifier := range options { 44 | if ordLower <= maxOrd { 45 | ml.Inputs[rune(ordLower)] = identifier 46 | ml.keys = append(ml.keys, ordLower) 47 | ordLower++ 48 | ml.highestOrd = ordLower 49 | } 50 | } 51 | } 52 | 53 | // Update takes a list of options, compares it to the existing list of options, and updates the menu if the new list 54 | // is different. This allows for updating a menu without creating a new one (which can mess with item ordering). 55 | // Returns true if the options were updated, false otherwise 56 | func (ml *MenuList) Update(options map[int]string) bool { 57 | // First things first, see if the updated options is the same as the original. If it is, do nothing 58 | eq := reflect.DeepEqual(options, ml.Options) 59 | 60 | if eq { 61 | return false 62 | } 63 | 64 | // The two are not equal. We need to rectify the items in the updated list with the original. This is a two step 65 | // process. First, update the inputs. For each input, if the identifier still exists in the new list, do nothing 66 | // If it does not exist, we'll clear out the identifier value. Next, we'll iterate over the new list, and 67 | // for each value that is not mapped to a key, we'll map it to one. This can be either an existing key, or 68 | // a new key that will be added 69 | 70 | // Before we do anything else, make sure we have maps to work with 71 | if ml.Inputs == nil { 72 | ml.Inputs = make(map[rune]int) 73 | } 74 | 75 | if ml.Options == nil { 76 | ml.Options = make(map[int]string) 77 | } 78 | 79 | for key, identifier := range ml.Inputs { 80 | // Check if the keys identifier is still in the updated list 81 | if _, ok := options[identifier]; !ok { 82 | // The identifier is no longer present in the updated list, so remove it from the key mapping 83 | ml.Inputs[key] = -1 84 | } 85 | } 86 | 87 | // Now, walk through the updated list, and assign new items to any empty keys. This will fill in any gaps in the 88 | // menu. 89 | for identifier := range options { 90 | // Loop through the inputs, looking for nulled slots (-1 for the identifier), also checking that the current 91 | // item is not already in the inputs 92 | placed := false 93 | if _, ok := ml.Options[identifier]; !ok { 94 | // This item is not currently in the existing list, and needs a spot in the input map 95 | for key, keyIdentifier := range ml.Inputs { 96 | if keyIdentifier == -1 { 97 | ml.Inputs[key] = identifier 98 | placed = true 99 | } 100 | } 101 | 102 | if !placed { 103 | // There was no free spot in an existing key, so we'll add a new one, based on the highest ord rune 104 | // used previously 105 | if ml.highestOrd <= maxOrd { 106 | ml.Inputs[rune(ml.highestOrd)] = identifier 107 | ml.keys = append(ml.keys, ml.highestOrd) 108 | ml.highestOrd++ 109 | placed = true 110 | } 111 | } 112 | 113 | // At this point, the item should have been placed. If it has not been, something has gone wrong 114 | if !placed { 115 | log.Print("Failed to place an item in the menu. Max Length likely exceeded.") 116 | } 117 | } 118 | } 119 | 120 | // Ensure we break any pointers here, in case options was passed as one. This can cause an odd bug where the 121 | // menu knows about new options before they have been passed for update. 122 | newOptions := make(map[int]string) 123 | 124 | for entity, name := range options { 125 | newOptions[entity] = name 126 | } 127 | 128 | // Finally, now that all the new items have been placed, and items that need to be removed have been removed, 129 | // set the menu options to the updated options list 130 | ml.Options = newOptions 131 | 132 | return true 133 | } 134 | 135 | // Print displays the options for the MenuList, sorted by the rune chosen to represent it. yOffset is the number of rows 136 | // to skip before printing, and xOffset, similarly, is the number of columns to skip before printing 137 | func (ml *MenuList) Print(height, width, xOffset, yOffset int) { 138 | lineStart := yOffset 139 | 140 | // Sort the index slice, this will allow for guaranteed printing order of the two data maps 141 | sort.Ints(ml.keys) 142 | 143 | for _, keyRune := range ml.keys { 144 | input := ml.Inputs[rune(keyRune)] 145 | PrintText(xOffset, lineStart, "("+string(keyRune)+")"+ml.Options[input], "", "", 0, 0) 146 | lineStart++ 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /gamemap/map.go: -------------------------------------------------------------------------------- 1 | package gamemap 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/camera" 5 | "github.com/gogue-framework/gogue/ui" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | // CoordinatePair represents a point in a 2D space 11 | type CoordinatePair struct { 12 | X int 13 | Y int 14 | } 15 | 16 | // Tile is a drawable feature on a gamemap. IT has a glyph for representation, and properties to determine if it blocks 17 | // movement, sight, and sound. Furthermore, each tile keeps track of whether the player has visited it, if its visible, 18 | // and if its been seen. Each tile also keeps track of any noises generated on it by entities. 19 | type Tile struct { 20 | Glyph ui.Glyph 21 | Blocked bool 22 | BlocksSight bool 23 | BlocksNoises bool 24 | Visited bool 25 | Explored bool 26 | Visible bool 27 | X int 28 | Y int 29 | Noises map[int]float64 30 | } 31 | 32 | // IsWall determines if a tile acts as a wall or not. A wall blocks sight and movement. If both of these criteria are 33 | // true, the tile is said to be a wall. 34 | func (t *Tile) IsWall() bool { 35 | if t.BlocksSight && t.Blocked { 36 | return true 37 | } 38 | 39 | return false 40 | } 41 | 42 | // GameMap is a 2D slice of Tile. The bounds of the map are determined by the width and height. FloorTiles keeps track 43 | // of all tiles in the GameMap that are marked as floors (does not block movement or sight, and can be occupied), this 44 | // useful for finding open tiles for spawning entities. 45 | type GameMap struct { 46 | Width int 47 | Height int 48 | Tiles [][]*Tile 49 | FloorTiles []*Tile 50 | } 51 | 52 | // InitializeMap sets up a GameMap for use. It sets the Tiles property of the GameMap to a 2D array of Tile objects, 53 | // with a width and height matching those set for the GameMap. It also initializes a random seed to use for map 54 | // generation 55 | func (m *GameMap) InitializeMap() { 56 | // Initialize a two dimensional array that will represent the current game map (of dimensions Width x Height) 57 | m.Tiles = make([][]*Tile, m.Width+1) 58 | for i := range m.Tiles { 59 | m.Tiles[i] = make([]*Tile, m.Height+1) 60 | } 61 | 62 | // Set a seed for procedural generation 63 | rand.Seed(time.Now().UTC().UnixNano()) 64 | } 65 | 66 | // Render draws a GameMap to the terminal, within a Camera viewport. It will only draw tiles from the GameMap that 67 | // visible to the player, and within the viewport of the Camera. If a Tile does not meet these criteria, it will not be 68 | // drawn. If a Tile is within the viewport of the Camera, but is outside the players FOV, and has been explored, it will 69 | // be drawn using the Tile.Glyph exploredColor. 70 | func (m *GameMap) Render(gameCamera *camera.GameCamera, newCameraX, newCameraY int) { 71 | 72 | gameCamera.MoveCamera(newCameraX, newCameraY, m.Width, m.Height) 73 | 74 | for x := 0; x < gameCamera.Width; x++ { 75 | for y := 0; y < gameCamera.Height; y++ { 76 | 77 | mapX, mapY := gameCamera.X+x, gameCamera.Y+y 78 | 79 | if mapX < 0 { 80 | mapX = 0 81 | } 82 | 83 | if mapY < 0 { 84 | mapY = 0 85 | } 86 | 87 | tile := m.Tiles[mapX][mapY] 88 | camX, camY := gameCamera.ToCameraCoordinates(mapX, mapY) 89 | 90 | // Print the tile, if it meets the following criteria: 91 | // 1. Its visible or explored 92 | // 2. It hasn't been printed yet. This will prevent over printing due to camera conversion 93 | if tile.Visible { 94 | ui.PrintGlyph(camX, camY, tile.Glyph, "", 0) 95 | } else if tile.Explored { 96 | ui.PrintGlyph(camX, camY, tile.Glyph, "", 0, true) 97 | } 98 | } 99 | } 100 | } 101 | 102 | // IsBlocked returns true if the Tile in the GameMap has its blocked property set to true. False otherwise. 103 | func (m *GameMap) IsBlocked(x, y int) bool { 104 | // Check to see if the provided coordinates contain a blocked tile 105 | if m.Tiles[x][y].Blocked { 106 | return true 107 | } 108 | 109 | return false 110 | } 111 | 112 | // BlocksNoises returns true if the Tile in the GameMap has its BlocksNoises property set to true. False otherwise. 113 | func (m *GameMap) BlocksNoises(x, y int) bool { 114 | // Check to see if the provided coordinates contain a tile that blocks noises 115 | if m.Tiles[x][y].BlocksNoises { 116 | return true 117 | } 118 | 119 | return false 120 | } 121 | 122 | // GetNeighbors will return a list of tiles that are directly next to the given coordinates. It can optionally exclude 123 | // blocked tiles 124 | func (m *GameMap) GetNeighbors(x, y int) []*Tile { 125 | neighbors := []*Tile{} 126 | sourceTile := m.Tiles[x][y] 127 | 128 | nX := 0 129 | nY := 0 130 | 131 | for i := -1; i <= 1; i++ { 132 | for j := -1; j <= 1; j++ { 133 | 134 | // Make sure the neighbor we're checking is within the bounds of the map 135 | if x+i < 0 || x+i > m.Width { 136 | continue 137 | } else { 138 | nX = x + i 139 | } 140 | 141 | if y+j < 0 || y+j > m.Height { 142 | continue 143 | } else { 144 | nY = y + j 145 | } 146 | 147 | // Exclude the source Tile 148 | if m.Tiles[nX][nY] != sourceTile { 149 | neighbors = append(neighbors, m.Tiles[nX][nY]) 150 | } 151 | } 152 | } 153 | 154 | return neighbors 155 | } 156 | 157 | // IsVisibleToPlayer returns true if the given position on the map is within the players vision radius 158 | func (m *GameMap) IsVisibleToPlayer(x, y int) bool { 159 | // Check to see if the given position on the map is visible to the player currently 160 | if m.Tiles[x][y].Visible { 161 | return true 162 | } 163 | 164 | return false 165 | } 166 | 167 | // IsVisibleAndExplored returns true if the player has visited the tile, and it is visible 168 | func (m *GameMap) IsVisibleAndExplored(x, y int) bool { 169 | if m.Tiles[x][y].Visible && m.Tiles[x][y].Explored { 170 | return true 171 | } 172 | 173 | return false 174 | } 175 | 176 | // HasNoises returns true if the given tile has any noises 177 | func (m *GameMap) HasNoises(x, y int) bool { 178 | if len(m.Tiles[x][y].Noises) > 0 { 179 | return true 180 | } 181 | 182 | return false 183 | } 184 | 185 | // GetAdjacentNoisesForEntity gets all adjacent tiles that have a noise associated with the given entity 186 | func (m *GameMap) GetAdjacentNoisesForEntity(entity, x, y int) map[*Tile]float64 { 187 | // Get a list of the neighboring tiles for the location 188 | tiles := m.GetNeighbors(x, y) 189 | 190 | noisyTiles := make(map[*Tile]float64) 191 | 192 | for _, tile := range tiles { 193 | for noiseEntity, noise := range m.Tiles[x][y].Noises { 194 | if noiseEntity == entity { 195 | noisyTiles[tile] = noise 196 | } 197 | } 198 | } 199 | 200 | return noisyTiles 201 | } 202 | -------------------------------------------------------------------------------- /gamemap/maptypes/cavern.go: -------------------------------------------------------------------------------- 1 | package maptypes 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/gamemap" 5 | "github.com/gogue-framework/gogue/ui" 6 | "math/rand" 7 | "sort" 8 | ) 9 | 10 | type bySize [][]*gamemap.Tile 11 | 12 | func (s bySize) Len() int { 13 | return len(s) 14 | } 15 | 16 | func (s bySize) Swap(i, j int) { 17 | s[i], s[j] = s[j], s[i] 18 | } 19 | 20 | func (s bySize) Less(i, j int) bool { 21 | return len(s[i]) < len(s[j]) 22 | } 23 | 24 | // GenerateCavern uses a cellular automata algorithm to create a fairly natural looking, 2D, cavern layout. It accepts 25 | // Glyphs representing the walls and floor. 26 | // The algorithm works in 6 steps. Step 1 fills the entire map with random wall and floor tiles, in roughly a 40/60 mix 27 | // Step 2 decides if each tile should remain a wall or become floor, based on its neighbors. Step 3 repeats step 2 a 28 | // number of times to smooth out the generated caverns. Step 4 seals up the edges of the map, so there are no paths off 29 | // the edge of the map. Step 5 uses a flood fill algorithm to find the largest cavern, and finally, step 6, fills all 30 | // smaller caverns. This algorithm is simple and does not connect unconnected caverns, and simply uses the largest 31 | // cavern as the play area. 32 | func GenerateCavern(surface *gamemap.GameMap, wallGlyph, floorGlyph ui.Glyph, smoothingPasses int) { 33 | // Step 1: Fill the map space with a random assortment of walls and floors. This uses a roughly 40/60 ratio in favor 34 | // of floors, as I've found that to produce the nicest results. 35 | 36 | for x := 0; x < surface.Width; x++ { 37 | for y := 0; y < surface.Height; y++ { 38 | state := rand.Intn(100) 39 | // All Tiles are created visible, by default. It is left up to the developer to set Tiles to not visible 40 | // as they see fit (say, through use of the FoV tools in Gogue). 41 | if state < 30 { 42 | surface.Tiles[x][y] = &gamemap.Tile{Glyph: wallGlyph, Blocked: true, BlocksSight: true, Visited: false, Explored: false, Visible: true, X: x, Y: y, Noises: make(map[int]float64)} 43 | } else { 44 | surface.Tiles[x][y] = &gamemap.Tile{Glyph: floorGlyph, Blocked: false, BlocksSight: false, Visited: false, Explored: false, Visible: true, X: x, Y: y, Noises: make(map[int]float64)} 45 | } 46 | } 47 | } 48 | 49 | // Step 2: Decide what should remain as walls. If 5 or more of a tiles immediate (within 1 space) neighbors are 50 | // walls, then make that tile a wall. If 2 or less of the tiles next closest (2 spaces away) neighbors are walls, 51 | // then make that tile a wall. Any other scenario, and the tile will become (or stay) a floor tile. 52 | // Make several passes on this to help smooth out the walls of the cave. 53 | for i := 0; i < 1; i++ { 54 | for x := 0; x < surface.Width; x++ { 55 | for y := 0; y < surface.Height-1; y++ { 56 | wallOneAway := countWallsNStepsAway(surface, 1, x, y) 57 | 58 | wallTwoAway := countWallsNStepsAway(surface, 1, x, y) 59 | 60 | if wallOneAway >= 5 || wallTwoAway <= 2 { 61 | surface.Tiles[x][y].Blocked = true 62 | surface.Tiles[x][y].BlocksSight = true 63 | surface.Tiles[x][y].Glyph = wallGlyph 64 | } else { 65 | surface.Tiles[x][y].Blocked = false 66 | surface.Tiles[x][y].BlocksSight = false 67 | surface.Tiles[x][y].Glyph = floorGlyph 68 | } 69 | } 70 | } 71 | } 72 | 73 | // Step 3: Make a few more passes, smoothing further, and removing any small or single tile, unattached walls. 74 | for i := 0; i < smoothingPasses; i++ { 75 | for x := 0; x < surface.Width; x++ { 76 | for y := 0; y < surface.Height-1; y++ { 77 | wallOneAway := countWallsNStepsAway(surface, 1, x, y) 78 | 79 | if wallOneAway >= 5 { 80 | surface.Tiles[x][y].Blocked = true 81 | surface.Tiles[x][y].BlocksSight = true 82 | surface.Tiles[x][y].Glyph = wallGlyph 83 | } else { 84 | surface.Tiles[x][y].Blocked = false 85 | surface.Tiles[x][y].BlocksSight = false 86 | surface.Tiles[x][y].Glyph = floorGlyph 87 | } 88 | } 89 | } 90 | } 91 | 92 | // Step 4: Seal up the edges of the map, so the player, and the following flood fill passes, cannot go beyond the 93 | // intended game area 94 | for x := 0; x < surface.Width; x++ { 95 | for y := 0; y < surface.Height; y++ { 96 | if x == 0 || x == surface.Width-1 || y == 0 || y == surface.Height-1 { 97 | surface.Tiles[x][y].Blocked = true 98 | surface.Tiles[x][y].BlocksSight = true 99 | surface.Tiles[x][y].Glyph = wallGlyph 100 | } 101 | } 102 | } 103 | 104 | // Step 5: Flood fill. This will find each individual cavern in the cave system, and add them to a list. It will 105 | // then find the largest one, and will make that as the main play area. The smaller caverns will be filled in. 106 | // In the future, it might make sense to tunnel between caverns, and apply a few more smoothing passes, to make 107 | // larger, more realistic caverns. 108 | 109 | var cavern []*gamemap.Tile 110 | var totalCavernArea []*gamemap.Tile 111 | var caverns [][]*gamemap.Tile 112 | var tile *gamemap.Tile 113 | var node *gamemap.Tile 114 | 115 | for x := 0; x < surface.Width-1; x++ { 116 | for y := 0; y < surface.Height-1; y++ { 117 | tile = surface.Tiles[x][y] 118 | 119 | // If the current tile is a wall, or has already been visited, ignore it and move on 120 | if !tile.Visited && !tile.IsWall() { 121 | // This is a non-wall, unvisited tile 122 | cavern = append(cavern, surface.Tiles[x][y]) 123 | 124 | for len(cavern) > 0 { 125 | // While the current node tile has valid neighbors, keep looking for more valid neighbors off of 126 | // each one 127 | node = cavern[len(cavern)-1] 128 | cavern = cavern[:len(cavern)-1] 129 | 130 | if !node.Visited && !node.IsWall() { 131 | // Mark the node as visited, and add it to the cavern area for this cavern 132 | node.Visited = true 133 | totalCavernArea = append(totalCavernArea, node) 134 | 135 | // Add the tile to the west, if valid 136 | if node.X-1 > 0 && !surface.Tiles[node.X-1][node.Y].IsWall() { 137 | cavern = append(cavern, surface.Tiles[node.X-1][node.Y]) 138 | } 139 | 140 | // Add the tile to east, if valid 141 | if node.X+1 < surface.Width && !surface.Tiles[node.X+1][node.Y].IsWall() { 142 | cavern = append(cavern, surface.Tiles[node.X+1][node.Y]) 143 | } 144 | 145 | // Add the tile to north, if valid 146 | if node.Y-1 > 0 && !surface.Tiles[node.X][node.Y-1].IsWall() { 147 | cavern = append(cavern, surface.Tiles[node.X][node.Y-1]) 148 | } 149 | 150 | // Add the tile to south, if valid 151 | if node.Y+1 < surface.Height && !surface.Tiles[node.X][node.Y+1].IsWall() { 152 | cavern = append(cavern, surface.Tiles[node.X][node.Y+1]) 153 | } 154 | } 155 | } 156 | 157 | // All non-wall tiles have been found for the current cavern, add it to the list, and start looking for 158 | // the next one 159 | caverns = append(caverns, totalCavernArea) 160 | totalCavernArea = nil 161 | } else { 162 | tile.Visited = true 163 | } 164 | } 165 | } 166 | 167 | // Sort the caverns slice by size. This will make the largest cavern last, which will then be removed from the list. 168 | // Then, fill in any remaining caverns (aside from the main one). This will ensure that there are no areas on the 169 | // map that the player cannot reach. 170 | sort.Sort(bySize(caverns)) 171 | 172 | // Take the largest cavern (The one being used as the map), and record it as a list of open floor tiles, since thats 173 | // what it represents. This will be used for content generation. 174 | surface.FloorTiles = caverns[len(caverns)-1] 175 | caverns = caverns[:len(caverns)-1] 176 | 177 | for i := 0; i < len(caverns); i++ { 178 | for j := 0; j < len(caverns[i]); j++ { 179 | caverns[i][j].Blocked = true 180 | caverns[i][j].BlocksSight = true 181 | caverns[i][j].Glyph = wallGlyph 182 | } 183 | } 184 | } 185 | 186 | func countWallsNStepsAway(surface *gamemap.GameMap, n int, x int, y int) int { 187 | // Return the number of wall tiles that are within n spaces of the given tile 188 | wallCount := 0 189 | 190 | for r := -n; r <= n; r++ { 191 | for c := -n; c <= n; c++ { 192 | if x+r >= surface.Width || x+r <= 0 || y+c >= surface.Height || y+c <= 0 { 193 | // Check if the current coordinates would be off the map. Off map coordinates count as a wall. 194 | wallCount++ 195 | } else if surface.Tiles[x+r][y+c].IsWall() { 196 | wallCount++ 197 | } 198 | } 199 | } 200 | 201 | return wallCount 202 | } 203 | -------------------------------------------------------------------------------- /ui/terminal.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | blt "github.com/gogue-framework/bearlibterminalgo" 5 | "strconv" 6 | ) 7 | 8 | // Not sure if converting these is going to prove useful or not 9 | // KEY just seems more natural than TK 10 | const ( 11 | KeyClose = blt.TK_CLOSE 12 | KeyRight = blt.TK_RIGHT 13 | KeyLeft = blt.TK_LEFT 14 | KeyUp = blt.TK_UP 15 | KeyDown = blt.TK_DOWN 16 | KeyA = blt.TK_A 17 | KeyB = blt.TK_B 18 | KeyC = blt.TK_C 19 | KeyD = blt.TK_D 20 | KeyE = blt.TK_E 21 | KeyF = blt.TK_F 22 | KeyG = blt.TK_G 23 | KeyH = blt.TK_H 24 | KeyI = blt.TK_I 25 | KeyJ = blt.TK_J 26 | KeyK = blt.TK_K 27 | KeyL = blt.TK_L 28 | KeyM = blt.TK_M 29 | KeyN = blt.TK_N 30 | KeyO = blt.TK_O 31 | KeyP = blt.TK_P 32 | KeyQ = blt.TK_Q 33 | KeyR = blt.TK_R 34 | KeyS = blt.TK_S 35 | KeyT = blt.TK_T 36 | KeyU = blt.TK_U 37 | KeyV = blt.TK_V 38 | KeyW = blt.TK_W 39 | KeyX = blt.TK_X 40 | KeyY = blt.TK_Y 41 | KeyZ = blt.TK_Z 42 | KeyComman = blt.TK_COMMA 43 | KeyEscape = blt.TK_ESCAPE 44 | KeyEnter = blt.TK_ENTER 45 | ) 46 | 47 | var ( 48 | // RuneKeyMapping maps keypresses to the respective Go Rune representations. This can be useful for checking 49 | // the rune associated with a keypress. 50 | RuneKeyMapping = map[int]rune{ 51 | blt.TK_A: 'a', 52 | blt.TK_B: 'b', 53 | blt.TK_C: 'c', 54 | blt.TK_D: 'd', 55 | blt.TK_E: 'e', 56 | blt.TK_F: 'f', 57 | blt.TK_G: 'g', 58 | blt.TK_H: 'h', 59 | blt.TK_I: 'i', 60 | blt.TK_J: 'j', 61 | blt.TK_K: 'k', 62 | blt.TK_L: 'l', 63 | blt.TK_M: 'm', 64 | blt.TK_N: 'n', 65 | blt.TK_O: 'o', 66 | blt.TK_P: 'p', 67 | blt.TK_Q: 'q', 68 | blt.TK_R: 'r', 69 | blt.TK_S: 's', 70 | blt.TK_T: 't', 71 | blt.TK_U: 'u', 72 | blt.TK_V: 'v', 73 | blt.TK_W: 'w', 74 | blt.TK_X: 'x', 75 | blt.TK_Y: 'y', 76 | blt.TK_Z: 'z', 77 | } 78 | compositionMode = 0 79 | ) 80 | 81 | func init() { 82 | 83 | } 84 | 85 | // InitConsole sets up a BearLibTerminal console window 86 | // The X and Y dimensions, title, and a fullscreen flag can all be provided 87 | // The console window is not actually rendered to the screen until Refresh is called 88 | func InitConsole(windowSizeX, windowSizeY int, title string, fullScreen bool) { 89 | blt.Open() 90 | 91 | // BearLibTerminal uses configuration strings to set itself up, so we need to build these strings here 92 | // First set up the string for window properties (size and title) 93 | size := "size=" + strconv.Itoa(windowSizeX) + "x" + strconv.Itoa(windowSizeY) 94 | consoleTitle := "title='" + title + "'" 95 | window := "window: " + size + "," + consoleTitle 96 | 97 | if fullScreen { 98 | consoleFullScreen := "fullscreen=true" 99 | window += "," + consoleFullScreen 100 | } 101 | 102 | // Now, put it all together 103 | blt.Set(window + "; ") 104 | blt.Composition(compositionMode) 105 | blt.Clear() 106 | } 107 | 108 | // SetPrimaryFont sets the font size and font to use. 109 | // If this method is not called, the default font and size for BearLibTerminal is used 110 | func SetPrimaryFont(fontSize int, fontPath string) { 111 | // Next, setup the font config string 112 | consoleFontSize := "size=" + strconv.Itoa(fontSize) 113 | font := "font: " + fontPath + ", " + consoleFontSize 114 | 115 | blt.Set(font + ";") 116 | blt.Clear() 117 | } 118 | 119 | // AddFont adds a named font to the console, that can be used when printing text, as an alternative to the 120 | // primary font. 121 | func AddFont(name, path string, fontSize int) { 122 | consoleFontSize := "size=" + strconv.Itoa(fontSize) 123 | font := name + " font: " + path + ", " + consoleFontSize 124 | 125 | blt.Set(font + ";") 126 | blt.Clear() 127 | } 128 | 129 | // SetCompositionMode sets the composition mode for drawing glyphs to the terminal. 0 is no composition, meaning the 130 | // entire cell will be replaced by the character drawn. 1 means the character drawn will be composed onto any lower 131 | // level characters. This is used to set the composition mode each time a character is printed. 132 | func SetCompositionMode(mode int) { 133 | if mode == blt.TK_OFF || mode == blt.TK_ON { 134 | compositionMode = mode 135 | } 136 | } 137 | 138 | // SetGlobalComposition sets the global terminal state of composition. Off means that each character printed to a cell 139 | // will, graphically, replace all other characters in that cell. Composition on means that each character printed to a 140 | // cell will be composed on top or beneath (depending on layer) any other characters present in that cell. 141 | func SetGlobalComposition(mode int) { 142 | if mode == blt.TK_OFF || mode == blt.TK_ON { 143 | blt.Composition(mode) 144 | } 145 | } 146 | 147 | // Refresh calls blt.Refresh on the current console window 148 | func Refresh() { 149 | blt.Refresh() 150 | } 151 | 152 | // CloseConsole calls blt.Close on the current console window 153 | func CloseConsole() { 154 | blt.Close() 155 | } 156 | 157 | // ClearArea clears a (rectangular) area of the terminal, starting at (x, y), and containing the area to 158 | // (x + width, y + height). 159 | func ClearArea(x, y, width, height, layer int) { 160 | blt.Layer(layer) 161 | blt.ClearArea(x, y, width, height) 162 | } 163 | 164 | // ClearWindow is just a wrapper call around ClearArea that clears the entire terminal window, from (0,0) to 165 | // (WindowWidth, WindowHeight) 166 | func ClearWindow(windowWidth, windowHeight, layer int) { 167 | ClearArea(0, 0, windowWidth, windowHeight, layer) 168 | } 169 | 170 | // PrintGlyph prints out a single character at the x, y coordinates provided, in the color provided, 171 | // and on the layer provided. If no layer is provided, layer 0 is used. 172 | func PrintGlyph(x, y int, g Glyph, backgroundColor string, layer int, useExploredColor ...bool) { 173 | // Set the layer first. If not provided, this defaults to 0, the base layer in BearLibTerminal 174 | blt.Layer(layer) 175 | 176 | if backgroundColor != "" { 177 | // If a background color was provided, set that 178 | // Background color can only be applied to the lowest layer 179 | blt.BkColor(blt.ColorFromName(backgroundColor)) 180 | } 181 | 182 | // Next, set the color to print in 183 | if len(useExploredColor) > 0 { 184 | blt.Color(blt.ColorFromName(g.ExploredColor())) 185 | } else { 186 | blt.Color(blt.ColorFromName(g.Color())) 187 | } 188 | 189 | // Finally, print the character at the provided coordinates 190 | blt.PrintExt(x, y, 0, 0, blt.TK_ALIGN_MIDDLE, string(g.Char())) 191 | } 192 | 193 | // PrintText will print a string of text, starting at the (X, Y) coords provided, using the color/background color 194 | // provided, on the layer provided. 195 | func PrintText(x, y int, text, color, backgroundColor string, layer int, splitWidth int) { 196 | // Set the layer first. If not provided, this defaults to 0, the base layer in BearLibTerminal 197 | blt.Layer(layer) 198 | 199 | if backgroundColor != "" { 200 | // If a background color was provided, set that 201 | // Background color can only be applied to the lowest layer 202 | blt.BkColor(blt.ColorFromName(backgroundColor)) 203 | } 204 | 205 | if color != "" { 206 | // If a color was set, use that, otherwise, default to white 207 | blt.Color(blt.ColorFromName(color)) 208 | } else { 209 | blt.Color(blt.ColorFromName("white")) 210 | } 211 | 212 | // Finally, print the character at the provided coordinates 213 | if splitWidth > 0 { 214 | // If a split width is specified, break the string into lines (on word boundaries), and print each line one cell 215 | // below the previous 216 | lines := SplitLines(text, splitWidth) 217 | 218 | lineY := y 219 | 220 | for _, line := range lines { 221 | blt.Print(x, lineY, line) 222 | lineY++ 223 | } 224 | } else { 225 | blt.Print(x, y, text) 226 | } 227 | } 228 | 229 | // ReadInput reads the next input event from the Input queue. 230 | // If the queue is empty, it will wait for an event in a blocking manner 231 | // if blocking=false is provided, the blocking behavior will not occur (if not input is found, execution continues, 232 | // rather than blocking execution until input is provided) 233 | func ReadInput(nonBlocking bool) int { 234 | if nonBlocking { 235 | // If non blocking reads are desired (say for a realtime game), check if there is an input in the Input queue 236 | // If there is, return it, otherwise, continue execution 237 | inputReady := blt.HasInput() 238 | 239 | if inputReady { 240 | return blt.Read() 241 | } 242 | 243 | return -1 244 | } 245 | 246 | // Default behavior is to use blocking reads 247 | return blt.Read() 248 | } 249 | -------------------------------------------------------------------------------- /djikstramaps/multiEntityMap.go: -------------------------------------------------------------------------------- 1 | package dijkstramaps 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/ecs" 5 | "github.com/gogue-framework/gogue/gamemap" 6 | ) 7 | 8 | // DMSource represents a single source in a multi-entity Dijkstra map 9 | type DMSource struct { 10 | entity int // The source entity ID 11 | X int 12 | Y int 13 | PrevX int 14 | PrevY int 15 | } 16 | 17 | // MultiEntityDijkstraMap is a Dijkstra map that tracks several entities. It is very similar to the single entity map, in that 18 | // each entity radiates a value outwards from it, the entity being the source. The main difference is that there can be 19 | // many sources, each affecting the values of the map. 20 | // 21 | // General idea is that each provided source will act as a single entity map source would, radiating values out from it. 22 | // But, since there will be multiple sources, values from other sources are never overwritten. This means that as the 23 | // values radiate out from a source, they will be calculated until they hit values from another source, and then stop. 24 | // In this way, a map representing the distance for each item will be generated. 25 | // 26 | // It must be noted, this type of map assumes that all sources are of the same entity type, otherwise, it wouldn't 27 | // really make sense. 28 | type MultiEntityDijkstraMap struct { 29 | sources map[int]DMSource 30 | mapType string 31 | ValuesMap [][]int 32 | visited map[*gamemap.Tile]bool 33 | } 34 | 35 | // NewMultiEntityMap creates a new multi-entity Dijkstra map. sourceList is the list of sources for the map to operate 36 | // from, and mapWidth and mapHeight indicate how large the resulting map should be (typically the size of the gamemap). 37 | // mapType is a string identifier for the map. 38 | func NewMultiEntityMap(sourceList map[int]DMSource, mapType string, mapWidth, mapHeight int) *MultiEntityDijkstraMap { 39 | medm := MultiEntityDijkstraMap{} 40 | medm.ValuesMap = make([][]int, mapWidth+1) 41 | medm.visited = make(map[*gamemap.Tile]bool) 42 | for i := range medm.ValuesMap { 43 | medm.ValuesMap[i] = make([]int, mapHeight+1) 44 | } 45 | 46 | medm.sources = sourceList 47 | 48 | // Set the map type 49 | medm.mapType = mapType 50 | 51 | return &medm 52 | } 53 | 54 | // AddSourceEntity adds a new entity to the source list for a multi-entity map 55 | func (medm *MultiEntityDijkstraMap) AddSourceEntity(source DMSource) { 56 | medm.sources[source.entity] = source 57 | } 58 | 59 | // UpdateSourceEntity updates an existing source with a new position, for example, if the source entity moved, the 60 | // Dijkstra map will need to be re-calculated based on the entities new position. 61 | func (medm *MultiEntityDijkstraMap) UpdateSourceEntity(entity, newX, newY int) { 62 | source := medm.sources[entity] 63 | 64 | source.PrevX = source.X 65 | source.PrevY = source.X 66 | 67 | source.X = newX 68 | source.Y = newY 69 | } 70 | 71 | // GenerateMap runs through creating a new multi-entity dijkstra map. This will calculate distances for every tile in 72 | // the map, from each source entity, to each other source entity. For example, if two source entities are three tiles 73 | // apart, the max distance for a tile between them would be two, as we only care about the maximum distance from any 74 | // entity. 75 | func (medm *MultiEntityDijkstraMap) GenerateMap(surface *gamemap.GameMap) { 76 | sourceList := make(map[int][]*gamemap.Tile, len(medm.sources)) 77 | medm.visited = make(map[*gamemap.Tile]bool) 78 | 79 | // Populate the sourceList 80 | for entity, source := range medm.sources { 81 | tile := surface.Tiles[source.X][source.Y] 82 | tileMap := []*gamemap.Tile{tile} 83 | 84 | // Also set the starting value for each source tile to zero 85 | medm.ValuesMap[source.X][source.Y] = 0 86 | 87 | sourceList[entity] = tileMap 88 | } 89 | 90 | // Now, iterate over each source, running a single round of BFS. If there are no tiles in the sources tileMap, no 91 | // further BFS search rounds need to be run. If all sources have no tiles in their tileMaps, then exit the loop, 92 | // as we're done 93 | finishedSources := []int{} 94 | 95 | for entity, tileMap := range sourceList { 96 | 97 | // If every source has been added to the finishedSources list, we're done, so exit the loop 98 | if len(finishedSources) == len(medm.sources) { 99 | break 100 | } 101 | 102 | // Check to see if this source has any tiles in its tileList. If it does not, it is done, and should be marked 103 | // as such. If there are tiles, continue processing 104 | if len(tileMap) == 0 && !ecs.IntInSlice(entity, finishedSources) { 105 | finishedSources = append(finishedSources, entity) 106 | } 107 | 108 | tile := tileMap[0] 109 | sourceList[entity] = tileMap[1:] 110 | sourceList[entity] = append(sourceList[entity], medm.SingleRoundBreadthFirstSearch(tile.X, tile.Y, surface)...) 111 | } 112 | } 113 | 114 | // SingleRoundBreadthFirstSearch runs a single round of a breadth first search algorithm. This will calculate distances 115 | // for one level of distance from a source entity, rather than calculating every distance at once. This is useful for 116 | // calculating multiple entity distances, as we can do them one level, and entity, at a time, and the see where the 117 | // distances may overlap. 118 | func (medm *MultiEntityDijkstraMap) SingleRoundBreadthFirstSearch(x, y int, surface *gamemap.GameMap) []*gamemap.Tile { 119 | // Check if this location has already been visited 120 | curTile := surface.Tiles[x][y] 121 | 122 | // Mark this location as visited, and increase the value by one 123 | // This will ensure that each subsequently further tile will have an increased value 124 | 125 | medm.visited[curTile] = true 126 | 127 | tileQueue := []*gamemap.Tile{} 128 | 129 | curValue := medm.ValuesMap[curTile.X][curTile.Y] + 1 130 | 131 | // Check all the immediate neighbors, and set values for them based on the current coordinates value 132 | // NorthWest 133 | neighborTile := surface.Tiles[curTile.X-1][curTile.Y-1] 134 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 135 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 136 | // add it to the tileQueue; We'll check its neighbors soon 137 | 138 | medm.visited[neighborTile] = true 139 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 140 | tileQueue = append(tileQueue, neighborTile) 141 | } 142 | 143 | // West 144 | neighborTile = surface.Tiles[curTile.X-1][curTile.Y] 145 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 146 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 147 | // add it to the tileQueue; We'll check its neighbors soon 148 | 149 | medm.visited[neighborTile] = true 150 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 151 | tileQueue = append(tileQueue, neighborTile) 152 | } 153 | 154 | // SouthWest 155 | neighborTile = surface.Tiles[curTile.X-1][curTile.Y+1] 156 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 157 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 158 | // add it to the tileQueue; We'll check its neighbors soon 159 | 160 | medm.visited[neighborTile] = true 161 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 162 | tileQueue = append(tileQueue, neighborTile) 163 | } 164 | 165 | // South 166 | neighborTile = surface.Tiles[curTile.X][curTile.Y+1] 167 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 168 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 169 | // add it to the tileQueue; We'll check its neighbors soon 170 | 171 | medm.visited[neighborTile] = true 172 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 173 | tileQueue = append(tileQueue, neighborTile) 174 | } 175 | 176 | // SouthEast 177 | neighborTile = surface.Tiles[curTile.X+1][curTile.Y+1] 178 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 179 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 180 | // add it to the tileQueue; We'll check its neighbors soon 181 | 182 | medm.visited[neighborTile] = true 183 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 184 | tileQueue = append(tileQueue, neighborTile) 185 | } 186 | 187 | // East 188 | neighborTile = surface.Tiles[curTile.X+1][curTile.Y] 189 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 190 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 191 | // add it to the tileQueue; We'll check its neighbors soon 192 | 193 | medm.visited[neighborTile] = true 194 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 195 | tileQueue = append(tileQueue, neighborTile) 196 | } 197 | 198 | // NorthEast 199 | neighborTile = surface.Tiles[curTile.X+1][curTile.Y-1] 200 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 201 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 202 | // add it to the tileQueue; We'll check its neighbors soon 203 | 204 | medm.visited[neighborTile] = true 205 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 206 | tileQueue = append(tileQueue, neighborTile) 207 | } 208 | 209 | // North 210 | neighborTile = surface.Tiles[curTile.X][curTile.Y-1] 211 | if !medm.visited[neighborTile] && !neighborTile.IsWall() { 212 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 213 | // add it to the tileQueue; We'll check its neighbors soon 214 | 215 | medm.visited[neighborTile] = true 216 | medm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 217 | tileQueue = append(tileQueue, neighborTile) 218 | } 219 | 220 | return tileQueue 221 | } 222 | -------------------------------------------------------------------------------- /djikstramaps/entityMap.go: -------------------------------------------------------------------------------- 1 | package dijkstramaps 2 | 3 | import ( 4 | "github.com/gogue-framework/gogue/gamemap" 5 | ) 6 | 7 | // EntityDijkstraMap is a Dijkstra map that centers around an entity. This could be the player, an item, a monster, a door, 8 | // etc. They are the simplest implementation, as the map radiates values from a single point, setting that point (the 9 | // location of the entity) as the source, meaning it will have the lowest value. These maps can optionally be inverted, 10 | // to make other entities move away from it (fleeing, for example). Each map will keep track of where the source entity 11 | // is, and where it was the previous turn. The map only needs to be recalculated if the source entity moved, otherwise, 12 | // the map can continually be re-used turn after turn without recalculation. Each map will also keep track of its type 13 | // (Player, health potion, mana potion, weapon, pack entity, terrifying, etc), and a master list of all maps can be 14 | // maintained, so each entity that cares about them can utilize the appropriate maps each turn. 15 | // Some examples: 16 | // The most obvious example is stalking the player. The player would be the source entity, and the map would be drawn 17 | // from her location each time movement occurred. Any monster or entity that cared about stalking the player can then 18 | // simply use the map values, rolling downhill towards the player each turn. 19 | // 20 | // A monster that cares about gold will check the map representing any gold entities on the map, and will move towards 21 | // those in the same manner they would move towards the player. If there are multiple entities as the map source, they 22 | // will move towards the closest. 23 | // 24 | // Multiple competing desires. If a monster wants gold, to kill the player, and pick up a health potion, they can 25 | // maintain a weight for each one of those desires (updated each turn, according to whats happening). These weights can 26 | // then be multiplied across all the values of every competing map. A positive number means they want to be far away 27 | // from that entity, 0 is indifference, and negative numbers mean high desire. Multiply values on the map by the desires 28 | // and you end up with a combined set of maps with values that reflect the monsters desires. 29 | type EntityDijkstraMap struct { 30 | source int // The source entity ID 31 | sourceX int 32 | sourceY int 33 | sourcePrevX int 34 | sourcePrevY int 35 | mapType string 36 | ValuesMap [][]int 37 | } 38 | 39 | // NewEntityMap creates a new EntityDijkstraMap. The source coordinates indicate where the Dijkstra map originates, 40 | // and the map width and height indicate how large the map should be (typically the same as the gamemap). mapType is 41 | // a string identifier to help show what this maps function is. 42 | func NewEntityMap(sourceEntity int, sourceX, sourceY int, mapType string, mapWidth, mapHeight int) *EntityDijkstraMap { 43 | edm := EntityDijkstraMap{} 44 | edm.ValuesMap = make([][]int, mapWidth+1) 45 | for i := range edm.ValuesMap { 46 | edm.ValuesMap[i] = make([]int, mapHeight+1) 47 | } 48 | 49 | // Set the source position 50 | edm.sourceX = sourceX 51 | edm.sourceY = sourceY 52 | 53 | // The previous positions will be the same initially 54 | edm.sourcePrevX = sourceX 55 | edm.sourcePrevY = sourceY 56 | 57 | // Set the map type 58 | edm.mapType = mapType 59 | 60 | return &edm 61 | } 62 | 63 | // UpdateSourceCoordinates sets the sourceX and sourceY properties to the latest values available, recording the 64 | // previous coordinates for later use. 65 | func (edm *EntityDijkstraMap) UpdateSourceCoordinates(x, y int) { 66 | edm.sourcePrevX = edm.sourceX 67 | edm.sourcePrevY = edm.sourceY 68 | 69 | edm.sourceX = x 70 | edm.sourceY = y 71 | } 72 | 73 | // UpdateMap checks the map to see if the update criteria (location of the source entity has changed) is met. If so, 74 | // the map will be regenerated. 75 | func (edm *EntityDijkstraMap) UpdateMap(surface *gamemap.GameMap) { 76 | if (edm.sourceX != edm.sourcePrevX) || (edm.sourceY != edm.sourcePrevY) { 77 | // The coordinates differ from the last previous set, meaning the entity has moved. Re-generate the map. 78 | edm.GenerateMap(surface) 79 | } 80 | } 81 | 82 | // GenerateMap will create a Dijkstra map, centered around the source entities current location. 83 | func (edm *EntityDijkstraMap) GenerateMap(surface *gamemap.GameMap) { 84 | // Starting from the source, flood fill every tile on the map, incrementing the value for each tile by one, 85 | // based on how far away from the source it is. Make a visited array first though (everything but the source is 86 | // unvisited initially. Also mark blocking tiles as visited. 87 | visited := make(map[*gamemap.Tile]bool) 88 | 89 | edm.BreadthFirstSearch(edm.sourceX, edm.sourceY, surface.Width, surface.Height, 0, visited, surface) 90 | } 91 | 92 | // BreadthFirstSearch implements a standard BFS algorithm to fill in values for each tile in the Dijkstra map. It will 93 | // visit all Tiles the same distance away from the source, before moving to the next step away, and repeating. Each tile 94 | // visited is assigned a distance from the source. Walls are ignored. 95 | func (edm *EntityDijkstraMap) BreadthFirstSearch(x, y, n, m, value int, visited map[*gamemap.Tile]bool, surface *gamemap.GameMap) { 96 | // Check if this location has already been visited 97 | tile := surface.Tiles[x][y] 98 | 99 | // Mark this location as visited, set the value for this location in the EDM, and increase the value by one 100 | // This will ensure that each subsequently further tile will have an increased value 101 | edm.ValuesMap[x][y] = value 102 | 103 | visited[tile] = true 104 | 105 | tileQueue := []*gamemap.Tile{tile} 106 | 107 | for len(tileQueue) > 0 { 108 | curTile := tileQueue[0] 109 | tileQueue = tileQueue[1:] 110 | 111 | curValue := edm.ValuesMap[curTile.X][curTile.Y] + 1 112 | 113 | // Check all the immediate neighbors, and set values for them based on the current coordinates value 114 | // NorthWest 115 | neighborTile := surface.Tiles[curTile.X-1][curTile.Y-1] 116 | if !visited[neighborTile] && !neighborTile.IsWall() { 117 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 118 | // add it to the tileQueue; We'll check its neighbors soon 119 | 120 | visited[neighborTile] = true 121 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 122 | tileQueue = append(tileQueue, neighborTile) 123 | } 124 | 125 | // West 126 | neighborTile = surface.Tiles[curTile.X-1][curTile.Y] 127 | if !visited[neighborTile] && !neighborTile.IsWall() { 128 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 129 | // add it to the tileQueue; We'll check its neighbors soon 130 | 131 | visited[neighborTile] = true 132 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 133 | tileQueue = append(tileQueue, neighborTile) 134 | } 135 | 136 | // SouthWest 137 | neighborTile = surface.Tiles[curTile.X-1][curTile.Y+1] 138 | if !visited[neighborTile] && !neighborTile.IsWall() { 139 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 140 | // add it to the tileQueue; We'll check its neighbors soon 141 | 142 | visited[neighborTile] = true 143 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 144 | tileQueue = append(tileQueue, neighborTile) 145 | } 146 | 147 | // South 148 | neighborTile = surface.Tiles[curTile.X][curTile.Y+1] 149 | if !visited[neighborTile] && !neighborTile.IsWall() { 150 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 151 | // add it to the tileQueue; We'll check its neighbors soon 152 | 153 | visited[neighborTile] = true 154 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 155 | tileQueue = append(tileQueue, neighborTile) 156 | } 157 | 158 | // SouthEast 159 | neighborTile = surface.Tiles[curTile.X+1][curTile.Y+1] 160 | if !visited[neighborTile] && !neighborTile.IsWall() { 161 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 162 | // add it to the tileQueue; We'll check its neighbors soon 163 | 164 | visited[neighborTile] = true 165 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 166 | tileQueue = append(tileQueue, neighborTile) 167 | } 168 | 169 | // East 170 | neighborTile = surface.Tiles[curTile.X+1][curTile.Y] 171 | if !visited[neighborTile] && !neighborTile.IsWall() { 172 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 173 | // add it to the tileQueue; We'll check its neighbors soon 174 | 175 | visited[neighborTile] = true 176 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 177 | tileQueue = append(tileQueue, neighborTile) 178 | } 179 | 180 | // NorthEast 181 | neighborTile = surface.Tiles[curTile.X+1][curTile.Y-1] 182 | if !visited[neighborTile] && !neighborTile.IsWall() { 183 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 184 | // add it to the tileQueue; We'll check its neighbors soon 185 | 186 | visited[neighborTile] = true 187 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 188 | tileQueue = append(tileQueue, neighborTile) 189 | } 190 | 191 | // North 192 | neighborTile = surface.Tiles[curTile.X][curTile.Y-1] 193 | if !visited[neighborTile] && !neighborTile.IsWall() { 194 | // This is a valid, un-visited, neighbor. Give it a value of (currentVal + 1), add it to the valueMap, and 195 | // add it to the tileQueue; We'll check its neighbors soon 196 | 197 | visited[neighborTile] = true 198 | edm.ValuesMap[neighborTile.X][neighborTile.Y] = curValue 199 | tileQueue = append(tileQueue, neighborTile) 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /ecs/controller.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | ) 8 | 9 | // Controller acts as a central coordinator for all entities within the game. In most cases, a single controller will 10 | // suffice for an entire game. The controller is responsible for creating new entities, managing components attached to 11 | // those entities, and registering and coordinating systems that act upon those entities. 12 | type Controller struct { 13 | systems map[reflect.Type]System 14 | sortedSystems map[int][]System 15 | priorityKeys []int 16 | nextEntityID int 17 | components map[reflect.Type][]int 18 | entities map[int]map[reflect.Type]Component 19 | deadEntities []int 20 | 21 | // The component map will keep track of what components are available 22 | componentMap map[string]Component 23 | } 24 | 25 | // NewController is a convenience/constructor method to properly initialize a new processor 26 | func NewController() *Controller { 27 | controller := Controller{} 28 | controller.systems = make(map[reflect.Type]System) 29 | controller.sortedSystems = make(map[int][]System) 30 | controller.priorityKeys = []int{} 31 | controller.nextEntityID = 0 32 | controller.components = make(map[reflect.Type][]int) 33 | controller.entities = make(map[int]map[reflect.Type]Component) 34 | controller.deadEntities = []int{} 35 | controller.componentMap = make(map[string]Component) 36 | 37 | return &controller 38 | } 39 | 40 | // CreateEntity creates a new entity in the world. An entity is simply a unique integer. 41 | // If any components are provided, they will be associated with the created entity 42 | func (c *Controller) CreateEntity(components []Component) int { 43 | entity := c.nextEntityID 44 | 45 | c.entities[c.nextEntityID] = make(map[reflect.Type]Component) 46 | 47 | if len(components) > 0 { 48 | for _, v := range components { 49 | c.AddComponent(c.nextEntityID, v) 50 | } 51 | } 52 | 53 | c.nextEntityID++ 54 | 55 | return entity 56 | } 57 | 58 | // DeleteEntity removes an entity, all component instances attached to that entity, and any components associations with 59 | // that entity 60 | func (c *Controller) DeleteEntity(entity int) { 61 | // First, delete all the component associations for the entity to be removed 62 | for k := range c.entities[entity] { 63 | c.RemoveComponent(entity, k) 64 | } 65 | 66 | // Then, delete the entity itself. The components have already been removed and disassociated with it, so a simple 67 | // delete will do here 68 | delete(c.entities, entity) 69 | } 70 | 71 | // MapComponentClass registers a component with the controller. This map of components gives the controller access to the 72 | // valid components for a game system, and allows for dynamic loading of components from the data loader. 73 | func (c *Controller) MapComponentClass(componentName string, component Component) { 74 | // TODO: Possible to overwrite existing components with old name... 75 | c.componentMap[componentName] = component 76 | } 77 | 78 | // GetMappedComponentClass returns a component class based on the name it was registered under. This allows for dyanamic 79 | // mapping of components to entities, for example, from the data loader. 80 | func (c *Controller) GetMappedComponentClass(componentName string) Component { 81 | if _, ok := c.componentMap[componentName]; ok { 82 | return c.componentMap[componentName] 83 | } 84 | 85 | // TODO: Add better (read: actual) error handling here 86 | fmt.Printf("Component[%s] not registered on Controller.\n", componentName) 87 | return nil 88 | } 89 | 90 | // HasMappedComponent returns true if the named component has been mapped, IE a component mapped 91 | // under "position" has been mapped to a PositionComponent 92 | func (c *Controller) HasMappedComponent(componentName string) bool { 93 | if _, ok := c.componentMap[componentName]; ok { 94 | return true 95 | } 96 | 97 | return false 98 | } 99 | 100 | // AddComponent adds a component to an entity. The component is added to the global list of components for the 101 | // processor, and also directly associated with the entity itself. This allows for flexible checking of components, 102 | // as you can check which entites are associated with a component, and vice versa. 103 | func (c *Controller) AddComponent(entity int, component Component) { 104 | // First, get the type of the component 105 | componentType := reflect.TypeOf(component) 106 | 107 | // Record that the component type is associated with the entity. 108 | c.components[componentType] = append(c.components[componentType], entity) 109 | 110 | // Now, check to see if the entity is already tracked in the controller entity list. If it is not, add it, and 111 | // associate the component with it 112 | if _, ok := c.entities[entity]; !ok { 113 | c.entities[entity] = make(map[reflect.Type]Component) 114 | } 115 | 116 | c.entities[entity][componentType] = component 117 | } 118 | 119 | // HasComponent checks a given entity to see if it has a given component associated with it 120 | func (c *Controller) HasComponent(entity int, componentType reflect.Type) bool { 121 | if _, ok := c.entities[entity][componentType]; ok { 122 | return true 123 | } 124 | 125 | return false 126 | } 127 | 128 | // GetComponent returns the component instance for a component type, if one exists for the provided entity 129 | func (c *Controller) GetComponent(entity int, componentType reflect.Type) Component { 130 | // Check the given entity has the provided component 131 | if c.HasComponent(entity, componentType) { 132 | return c.entities[entity][componentType] 133 | } 134 | 135 | return nil 136 | } 137 | 138 | // GetEntity gets a specific entity, and all of its component instances 139 | func (c *Controller) GetEntity(entity int) map[reflect.Type]Component { 140 | for i := range c.entities { 141 | if i == entity { 142 | return c.entities[entity] 143 | } 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // GetEntities returns a map of all entities and their component instances 150 | func (c *Controller) GetEntities() map[int]map[reflect.Type]Component { 151 | return c.entities 152 | } 153 | 154 | // GetEntitiesWithComponent returns a list of all entities with a given component attached 155 | // TODO: Allow for passing a list of components 156 | func (c *Controller) GetEntitiesWithComponent(componentType reflect.Type) []int { 157 | entitiesWithComponent := make([]int, 0) 158 | for entity := range c.entities { 159 | if c.HasComponent(entity, componentType) { 160 | entitiesWithComponent = append(entitiesWithComponent, entity) 161 | } 162 | } 163 | 164 | return entitiesWithComponent 165 | } 166 | 167 | // UpdateComponent updates a component on an entity with a new version of the same component 168 | func (c *Controller) UpdateComponent(entity int, componentType reflect.Type, newComponent Component) int { 169 | // First, remove the component in question (Don't actually update things, but rather remove and replace) 170 | c.RemoveComponent(entity, componentType) 171 | 172 | // Next, replace the removed component with the updated one 173 | c.AddComponent(entity, newComponent) 174 | 175 | return entity 176 | } 177 | 178 | // RemoveComponent will delete a component instance from an entity, based on component type. It will also remove the 179 | // association between the component and the entity, and remove the component from the processor completely if no 180 | // other entities are using it. 181 | func (c *Controller) RemoveComponent(entity int, componentType reflect.Type) int { 182 | // Find the index of the entity to operate on in the components slice 183 | index := -1 184 | for i, v := range c.components[componentType] { 185 | if v == entity { 186 | index = i 187 | } 188 | } 189 | 190 | // If the component was found on the entity, remove the association between the component and the entity 191 | if index != -1 { 192 | c.components[componentType] = append(c.components[componentType][:index], c.components[componentType][index+1:]...) 193 | // If this was the last entity associated with the component, remove the component entry as well 194 | if len(c.components[componentType]) == 0 { 195 | delete(c.components, componentType) 196 | } 197 | } 198 | 199 | // Now, remove the component instance from the entity 200 | delete(c.entities[entity], componentType) 201 | 202 | return entity 203 | } 204 | 205 | // AddSystem registers a system to the controller. A priority can be provided, and systems will be processed in 206 | // numeric order, low to high. If multiple systems are registered as the same priority, they will be randomly run within 207 | // that priority group. 208 | func (c *Controller) AddSystem(system System, priority int) { 209 | systemType := reflect.TypeOf(system) 210 | 211 | if _, ok := c.systems[systemType]; !ok { 212 | // A system of this type has not been added yet, so add it to the systems list 213 | c.systems[systemType] = system 214 | 215 | // Now, append the system to a special list that will be used for sorting by priority 216 | if !IntInSlice(priority, c.priorityKeys) { 217 | c.priorityKeys = append(c.priorityKeys, priority) 218 | } 219 | c.sortedSystems[priority] = append(c.sortedSystems[priority], system) 220 | sort.Ints(c.priorityKeys) 221 | } else { 222 | fmt.Printf("A system of type %v was already added to the controller %v!", systemType, c) 223 | } 224 | } 225 | 226 | // Process kicks off system processing for all systems attached to the controller. Systems will be processed in the 227 | // order they are found, or if they have a priority, in priority order. If there is a mix of systems with priority and 228 | // without, systems with priority will be processed first (in order). 229 | func (c *Controller) Process(excludedSystems []reflect.Type) { 230 | for _, key := range c.priorityKeys { 231 | for _, system := range c.sortedSystems[key] { 232 | systemType := reflect.TypeOf(system) 233 | 234 | // Check if the current system type was marked as excluded on this call. If it was, not process it. 235 | if !TypeInSlice(systemType, excludedSystems) { 236 | system.Process() 237 | } 238 | } 239 | } 240 | } 241 | 242 | // HasSystem checks the controller to see if it has a given system associated with it 243 | func (c *Controller) HasSystem(systemType reflect.Type) bool { 244 | if _, ok := c.systems[systemType]; ok { 245 | return true 246 | } 247 | 248 | return false 249 | } 250 | 251 | // ProcessSystem allows for on demand processing of individual systems, rather than processing all at once via Process 252 | func (c *Controller) ProcessSystem(systemType reflect.Type) { 253 | if c.HasSystem(systemType) { 254 | system := c.systems[systemType] 255 | system.Process() 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /ecs/ecs_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | type PositionComponent struct { 10 | X int 11 | Y int 12 | } 13 | 14 | func (pc PositionComponent) TypeOf() reflect.Type { 15 | return reflect.TypeOf(pc) 16 | } 17 | 18 | type AppearanceComponent struct { 19 | Appearance string 20 | } 21 | 22 | func (ac AppearanceComponent) TypeOf() reflect.Type { 23 | return reflect.TypeOf(ac) 24 | } 25 | 26 | type RandomComponent struct { 27 | prop string 28 | } 29 | 30 | func (rc RandomComponent) TypeOf() reflect.Type { 31 | return reflect.TypeOf(rc) 32 | } 33 | 34 | func TestNewController(t *testing.T) { 35 | controller := NewController() 36 | 37 | assert.Equal(t, controller.nextEntityID, 0, "controller did not init properly") 38 | } 39 | 40 | func TestCreateNewEntity(t *testing.T) { 41 | controller := NewController() 42 | 43 | controller.CreateEntity([]Component{}) 44 | 45 | assert.Equal(t, controller.nextEntityID, 1, "expected nextEntityId to increment") 46 | 47 | assert.Equal(t, len(controller.entities), 1, "expected entities array to contain 1 entity") 48 | 49 | // The global list of components should be empty, as we didn't init this entity with 50 | // any components 51 | components := controller.components 52 | assert.Equal(t, len(components), 0, "components list should be empty") 53 | 54 | // Checking for a component on the new entity should return false, as it was not init'd 55 | // with one 56 | assert.False(t, controller.HasComponent(0, reflect.TypeOf(PositionComponent{})), "entity should not have component PositionComponent") 57 | } 58 | 59 | func TestGetEntity(t *testing.T) { 60 | controller := NewController() 61 | 62 | entity := controller.CreateEntity([]Component{}) 63 | 64 | assert.NotNil(t, controller.GetEntity(entity), "entity not retrieved") 65 | assert.Nil(t, controller.GetEntity(1000), "incorrect entity retrieved") 66 | } 67 | 68 | func TestCreateNewEntityWithComponent(t *testing.T) { 69 | controller := NewController() 70 | 71 | entity := controller.CreateEntity([]Component{PositionComponent{}}) 72 | 73 | assert.Equal(t, entity, 0, "expected entity to be 0") 74 | assert.Equal(t, controller.nextEntityID, 1, "expected nextEntityId to increment") 75 | 76 | components := controller.components 77 | assert.Equal(t, len(components), 1, "components should contain 1 component") 78 | assert.True(t, controller.HasComponent(0, reflect.TypeOf(PositionComponent{}))) 79 | 80 | // Also check that we can add new components after an entity has been created 81 | secondEntity := controller.CreateEntity([]Component{}) 82 | controller.AddComponent(secondEntity, PositionComponent{}) 83 | 84 | components = controller.components 85 | assert.Equal(t, len(components), 1, "components should contain 1 component") 86 | assert.True(t, controller.HasComponent(1, reflect.TypeOf(PositionComponent{}))) 87 | } 88 | 89 | func TestDeleteEntity(t *testing.T) { 90 | controller := NewController() 91 | 92 | entity := controller.CreateEntity([]Component{}) 93 | assert.Equal(t, entity, 0, "entity was not properly created") 94 | assert.Equal(t, len(controller.entities), 1, "entity not added to controller") 95 | 96 | controller.DeleteEntity(entity) 97 | assert.Equal(t, len(controller.entities), 0, "entity was not deleted") 98 | assert.False(t, controller.HasComponent(entity, reflect.TypeOf(PositionComponent{})), "entity has component") 99 | } 100 | 101 | func TestComponentMapping(t *testing.T) { 102 | controller := NewController() 103 | 104 | controller.MapComponentClass("position", PositionComponent{}) 105 | assert.True(t, controller.HasMappedComponent("position"), "component position not present") 106 | assert.False(t, controller.HasMappedComponent("movement"), "component movement present") 107 | } 108 | 109 | func TestAddComponent(t *testing.T) { 110 | controller := NewController() 111 | 112 | entity := controller.CreateEntity([]Component{}) 113 | 114 | controller.AddComponent(entity, PositionComponent{}) 115 | 116 | assert.True(t, controller.HasComponent(entity, reflect.TypeOf(PositionComponent{})), "position component missing") 117 | assert.NotNil(t, controller.GetComponent(entity, reflect.TypeOf(PositionComponent{})), "could not get position component") 118 | } 119 | 120 | func TestGetEntitiesWithComponent(t *testing.T) { 121 | controller := NewController() 122 | 123 | entity := controller.CreateEntity([]Component{PositionComponent{}, AppearanceComponent{}}) 124 | entity2 := controller.CreateEntity([]Component{AppearanceComponent{}}) 125 | entity3 := controller.CreateEntity([]Component{PositionComponent{}}) 126 | 127 | entityList := controller.GetEntitiesWithComponent(PositionComponent{}.TypeOf()) 128 | assert.Contains(t, entityList, entity) 129 | assert.Contains(t, entityList, entity3) 130 | 131 | entityList = controller.GetEntitiesWithComponent(AppearanceComponent{}.TypeOf()) 132 | assert.Contains(t, entityList, entity) 133 | assert.Contains(t, entityList, entity2) 134 | 135 | entityList = controller.GetEntitiesWithComponent(RandomComponent{}.TypeOf()) 136 | assert.Empty(t, entityList) 137 | 138 | } 139 | 140 | func TestUpdateComponent(t *testing.T) { 141 | controller := NewController() 142 | 143 | playerPosition := PositionComponent{ 144 | X: 0, 145 | Y: 0, 146 | } 147 | 148 | entity := controller.CreateEntity([]Component{playerPosition}) 149 | component := controller.GetComponent(entity, PositionComponent{}.TypeOf()).(PositionComponent) 150 | assert.Equal(t, component.X, 0) 151 | assert.Equal(t, component.Y, 0) 152 | 153 | // Change the value of X and Y in the PositionComponent 154 | playerPosition = PositionComponent{ 155 | X: 10, 156 | Y: 5, 157 | } 158 | 159 | entity = controller.UpdateComponent(entity, PositionComponent{}.TypeOf(), playerPosition) 160 | newComponent := controller.GetComponent(entity, PositionComponent{}.TypeOf()).(PositionComponent) 161 | assert.Equal(t, newComponent.X, 10) 162 | assert.Equal(t, newComponent.Y, 5) 163 | assert.True(t, controller.HasComponent(entity, PositionComponent{}.TypeOf())) 164 | 165 | monsterPosition := PositionComponent{ 166 | X: 1, 167 | Y: 0, 168 | } 169 | 170 | // Check calling UpdateComponent with a component the entity does not have. 171 | entity2 := controller.CreateEntity([]Component{}) 172 | entity = controller.UpdateComponent(entity, PositionComponent{}.TypeOf(), monsterPosition) 173 | assert.False(t, controller.HasComponent(entity2, PositionComponent{}.TypeOf())) 174 | 175 | } 176 | 177 | func TestRemoveComponent(t *testing.T) { 178 | controller := NewController() 179 | 180 | entity := controller.CreateEntity([]Component{PositionComponent{}}) 181 | assert.True(t, controller.HasComponent(entity, PositionComponent{}.TypeOf())) 182 | 183 | controller.RemoveComponent(entity, PositionComponent{}.TypeOf()) 184 | assert.False(t, controller.HasComponent(entity, PositionComponent{}.TypeOf())) 185 | 186 | // Re-add the component to ensure it does not get removed in the next section 187 | controller.AddComponent(entity, PositionComponent{}) 188 | assert.True(t, controller.HasComponent(entity, PositionComponent{}.TypeOf())) 189 | 190 | // Attempt to remove a component that doesn't exist. No errors should be raised 191 | entity2 := controller.CreateEntity([]Component{}) 192 | assert.False(t, controller.HasComponent(entity2, PositionComponent{}.TypeOf())) 193 | controller.RemoveComponent(entity2, PositionComponent{}.TypeOf()) 194 | assert.False(t, controller.HasComponent(entity2, PositionComponent{}.TypeOf())) 195 | assert.True(t, controller.HasComponent(entity, PositionComponent{}.TypeOf())) 196 | } 197 | 198 | // System tests 199 | 200 | type TestSystem struct { 201 | SystemRun bool 202 | } 203 | 204 | func (ts *TestSystem) Process() { 205 | ts.SystemRun = true 206 | } 207 | 208 | type AnotherSystem struct { 209 | SystemRun bool 210 | } 211 | 212 | func (as *AnotherSystem) Process() { 213 | as.SystemRun = true 214 | } 215 | 216 | type OneMoreSystem struct { 217 | SystemRun bool 218 | } 219 | 220 | func (oms *OneMoreSystem) Process() { 221 | oms.SystemRun = true 222 | } 223 | 224 | func TestAddSystem(t *testing.T) { 225 | controller := NewController() 226 | 227 | // Add two systems, with different priorities 228 | controller.AddSystem(&TestSystem{SystemRun: false}, 1) 229 | controller.AddSystem(&AnotherSystem{SystemRun: false}, 2) 230 | controller.AddSystem(&OneMoreSystem{SystemRun: false}, 1) 231 | 232 | assert.True(t, controller.HasSystem(reflect.TypeOf(&TestSystem{}))) 233 | assert.True(t, controller.HasSystem(reflect.TypeOf(&AnotherSystem{}))) 234 | 235 | // Make sure the ordering of systems by priority is correct 236 | assert.Equal(t, len(controller.sortedSystems[1]), 2) 237 | assert.Equal(t, len(controller.sortedSystems[2]), 1) 238 | } 239 | 240 | func TestProcessSystems(t *testing.T) { 241 | controller := NewController() 242 | 243 | system1 := &TestSystem{SystemRun: false} 244 | system2 := &AnotherSystem{SystemRun: false} 245 | system3 := &OneMoreSystem{SystemRun: false} 246 | 247 | controller.AddSystem(system1, 1) 248 | controller.AddSystem(system2, 2) 249 | controller.AddSystem(system3, 3) 250 | 251 | controller.Process([]reflect.Type{}) 252 | 253 | assert.True(t, system1.SystemRun) 254 | assert.True(t, system2.SystemRun) 255 | assert.True(t, system3.SystemRun) 256 | 257 | // Create a new controller, and add all systems in the priority 258 | controller2 := NewController() 259 | 260 | system1 = &TestSystem{SystemRun: false} 261 | system2 = &AnotherSystem{SystemRun: false} 262 | system3 = &OneMoreSystem{SystemRun: false} 263 | 264 | controller2.AddSystem(system1, 1) 265 | controller2.AddSystem(system2, 1) 266 | controller2.AddSystem(system3, 1) 267 | 268 | controller2.Process([]reflect.Type{}) 269 | 270 | assert.True(t, system1.SystemRun) 271 | assert.True(t, system2.SystemRun) 272 | assert.True(t, system3.SystemRun) 273 | 274 | // Create a new controller, and add all systems, but exclude one from processing 275 | controller3 := NewController() 276 | 277 | system1 = &TestSystem{SystemRun: false} 278 | system2 = &AnotherSystem{SystemRun: false} 279 | system3 = &OneMoreSystem{SystemRun: false} 280 | 281 | controller3.AddSystem(system1, 1) 282 | controller3.AddSystem(system2, 1) 283 | controller3.AddSystem(system3, 1) 284 | 285 | // Exclude OneMoreSystem systems from processing 286 | controller3.Process([]reflect.Type{reflect.TypeOf(&OneMoreSystem{})}) 287 | 288 | assert.True(t, system1.SystemRun) 289 | assert.True(t, system2.SystemRun) 290 | assert.False(t, system3.SystemRun) 291 | 292 | } 293 | 294 | func TestProcessSingleSystem(t *testing.T) { 295 | controller := NewController() 296 | 297 | system1 := &TestSystem{SystemRun: false} 298 | system2 := &AnotherSystem{SystemRun: false} 299 | system3 := &OneMoreSystem{SystemRun: false} 300 | 301 | controller.AddSystem(system1, 1) 302 | controller.AddSystem(system2, 1) 303 | 304 | controller.ProcessSystem(reflect.TypeOf(&TestSystem{})) 305 | 306 | assert.True(t, system1.SystemRun) 307 | assert.False(t, system2.SystemRun) 308 | 309 | controller.ProcessSystem(reflect.TypeOf(&OneMoreSystem{})) 310 | assert.False(t, system3.SystemRun) 311 | } 312 | --------------------------------------------------------------------------------