├── .gitignore ├── resources ├── fonts │ ├── onyx.ttf │ ├── Inversionz.ttf │ ├── Inversionz Italic.ttf │ ├── Inversionz Unboxed.ttf │ ├── SourceCodePro-Regular.ttf │ └── Inversionz Unboxed Italic.ttf ├── images │ ├── grid │ │ ├── ph_map.png │ │ ├── 4x4 │ │ │ ├── poison.png │ │ │ ├── selection.png │ │ │ ├── square_box.png │ │ │ ├── square_fill.png │ │ │ ├── square_large.png │ │ │ ├── square_medium.png │ │ │ └── square_small.png │ │ ├── 5x5 │ │ │ ├── poison.png │ │ │ ├── selection.png │ │ │ ├── square_box.png │ │ │ ├── square_fill.png │ │ │ ├── square_large.png │ │ │ ├── square_medium.png │ │ │ └── square_small.png │ │ └── 8x8 │ │ │ ├── poison.png │ │ │ ├── selection.png │ │ │ ├── square_box.png │ │ │ ├── square_fill.png │ │ │ ├── square_large.png │ │ │ ├── square_medium.png │ │ │ └── square_small.png │ ├── pause_button.png │ └── play_button.png └── resources.go ├── food ├── api.go └── food.go ├── environment └── api.go ├── settings ├── small.json ├── big.json ├── custom.json └── default.json ├── organism ├── info.go ├── api.go ├── traits.go └── organism.go ├── decision ├── node_test.go ├── utils.go ├── tree.go ├── constants.go └── node.go ├── go.mod ├── main.go ├── config ├── options.go └── globals.go ├── .vscode └── launch.json ├── manager ├── updates.go ├── requests.go ├── food.go ├── environment.go └── organism.go ├── ux ├── debug.go ├── interface.go ├── panel.go ├── graph.go └── grid.go ├── runner └── runner.go ├── utils └── geometry.go ├── go.sum ├── README.md └── simulation └── simulation.go /.gitignore: -------------------------------------------------------------------------------- 1 | compiled 2 | .gitignore 3 | *.idea 4 | *.iml 5 | protozoa 6 | -------------------------------------------------------------------------------- /resources/fonts/onyx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/fonts/onyx.ttf -------------------------------------------------------------------------------- /resources/fonts/Inversionz.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/fonts/Inversionz.ttf -------------------------------------------------------------------------------- /resources/images/grid/ph_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/ph_map.png -------------------------------------------------------------------------------- /resources/images/pause_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/pause_button.png -------------------------------------------------------------------------------- /resources/images/play_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/play_button.png -------------------------------------------------------------------------------- /resources/fonts/Inversionz Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/fonts/Inversionz Italic.ttf -------------------------------------------------------------------------------- /resources/fonts/Inversionz Unboxed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/fonts/Inversionz Unboxed.ttf -------------------------------------------------------------------------------- /resources/images/grid/4x4/poison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/4x4/poison.png -------------------------------------------------------------------------------- /resources/images/grid/5x5/poison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/5x5/poison.png -------------------------------------------------------------------------------- /resources/images/grid/8x8/poison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/8x8/poison.png -------------------------------------------------------------------------------- /resources/images/grid/4x4/selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/4x4/selection.png -------------------------------------------------------------------------------- /resources/images/grid/4x4/square_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/4x4/square_box.png -------------------------------------------------------------------------------- /resources/images/grid/5x5/selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/5x5/selection.png -------------------------------------------------------------------------------- /resources/images/grid/5x5/square_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/5x5/square_box.png -------------------------------------------------------------------------------- /resources/images/grid/8x8/selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/8x8/selection.png -------------------------------------------------------------------------------- /resources/images/grid/8x8/square_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/8x8/square_box.png -------------------------------------------------------------------------------- /resources/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /resources/images/grid/4x4/square_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/4x4/square_fill.png -------------------------------------------------------------------------------- /resources/images/grid/4x4/square_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/4x4/square_large.png -------------------------------------------------------------------------------- /resources/images/grid/4x4/square_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/4x4/square_medium.png -------------------------------------------------------------------------------- /resources/images/grid/4x4/square_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/4x4/square_small.png -------------------------------------------------------------------------------- /resources/images/grid/5x5/square_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/5x5/square_fill.png -------------------------------------------------------------------------------- /resources/images/grid/5x5/square_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/5x5/square_large.png -------------------------------------------------------------------------------- /resources/images/grid/5x5/square_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/5x5/square_medium.png -------------------------------------------------------------------------------- /resources/images/grid/5x5/square_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/5x5/square_small.png -------------------------------------------------------------------------------- /resources/images/grid/8x8/square_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/8x8/square_fill.png -------------------------------------------------------------------------------- /resources/images/grid/8x8/square_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/8x8/square_large.png -------------------------------------------------------------------------------- /resources/images/grid/8x8/square_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/8x8/square_medium.png -------------------------------------------------------------------------------- /resources/images/grid/8x8/square_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/images/grid/8x8/square_small.png -------------------------------------------------------------------------------- /resources/fonts/Inversionz Unboxed Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zebbeni/protozoa/HEAD/resources/fonts/Inversionz Unboxed Italic.ttf -------------------------------------------------------------------------------- /food/api.go: -------------------------------------------------------------------------------- 1 | package food 2 | 3 | import "github.com/Zebbeni/protozoa/utils" 4 | 5 | // API provides functions to look up or update information for the sim state 6 | type API interface { 7 | AddFoodUpdate(p utils.Point) 8 | } 9 | -------------------------------------------------------------------------------- /environment/api.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import "github.com/Zebbeni/protozoa/utils" 4 | 5 | // API provides functions to look up information about the sim state 6 | type API interface { 7 | Cycle() int 8 | AddPhUpdate(p utils.Point) 9 | } 10 | -------------------------------------------------------------------------------- /settings/small.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial_food": 0, 3 | "initial_organisms": 250, 4 | "grid_unit_size": 8, 5 | "grid_width": 1000, 6 | "grid_height": 800, 7 | "grid_units_wide": 125, 8 | "grid_units_high": 100, 9 | "screen_width": 1400, 10 | "screen_height": 800 11 | } 12 | -------------------------------------------------------------------------------- /settings/big.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial_food": 0, 3 | "initial_organisms": 1000, 4 | "grid_unit_size": 4, 5 | "grid_width": 1000, 6 | "grid_height": 800, 7 | "grid_units_wide": 250, 8 | "grid_units_high": 200, 9 | "screen_width": 1400, 10 | "screen_height": 800, 11 | 12 | "pool_width": 25, 13 | "pool_height": 25 14 | } 15 | -------------------------------------------------------------------------------- /food/food.go: -------------------------------------------------------------------------------- 1 | package food 2 | 3 | import ( 4 | u "github.com/Zebbeni/protozoa/utils" 5 | ) 6 | 7 | // Item contains an x, y coordinate and a food value 8 | type Item struct { 9 | Point u.Point 10 | Value int 11 | } 12 | 13 | // NewItem creates a new food Item with a given point and value 14 | func NewItem(point u.Point, value int) *Item { 15 | return &Item{ 16 | Point: point, 17 | Value: value, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /organism/info.go: -------------------------------------------------------------------------------- 1 | package organism 2 | 3 | import ( 4 | "github.com/Zebbeni/protozoa/decision" 5 | "github.com/Zebbeni/protozoa/utils" 6 | "github.com/lucasb-eyer/go-colorful" 7 | ) 8 | 9 | // Info contains all information relevant to rendering an organism 10 | type Info struct { 11 | ID int 12 | Health float64 13 | Location utils.Point 14 | Size float64 15 | Action decision.Action 16 | AncestorID int 17 | Color colorful.Color 18 | Age int 19 | Children int 20 | PhEffect float64 21 | } 22 | -------------------------------------------------------------------------------- /decision/node_test.go: -------------------------------------------------------------------------------- 1 | package decision 2 | 3 | import "testing" 4 | 5 | func TestUpdateNodeIDs(t *testing.T) { 6 | testCases := []struct { 7 | tree *Tree 8 | expected string 9 | }{ 10 | {TreeFromAction(ActAttack), "00"}, 11 | {&Tree{ID: "080002", Node: &Node{NodeType: CanMove, YesNode: NodeFromAction(ActAttack), NoNode: NodeFromAction(ActEat)}}, "080002"}, 12 | } 13 | 14 | for index, testCase := range testCases { 15 | actual := testCase.tree.Serialize() 16 | expected := testCase.expected 17 | if actual != expected { 18 | t.Errorf("tree ID %d was %s, expected %s\n", index, actual, expected) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Zebbeni/protozoa 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.2.7 7 | github.com/lucasb-eyer/go-colorful v1.2.0 8 | golang.org/x/image v0.5.0 9 | ) 10 | 11 | require ( 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect 13 | github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5 // indirect 16 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect 17 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 18 | golang.org/x/text v0.7.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | 8 | "github.com/Zebbeni/protozoa/config" 9 | "github.com/Zebbeni/protozoa/runner" 10 | ) 11 | 12 | var opts *config.Options 13 | 14 | func main() { 15 | runner.RunSimulation(opts) 16 | } 17 | 18 | func init() { 19 | opts = config.GetOptions() 20 | 21 | if opts.DumpConfig { 22 | g := config.GetDefaultGlobals() 23 | config.DumpGlobals(&g, os.Stdout) 24 | os.Exit(0) 25 | } 26 | 27 | var globals *config.Globals 28 | if opts.ConfigFile != "" { 29 | file := config.LoadFile(opts.ConfigFile) 30 | globals = config.LoadGlobals(file) 31 | } else { 32 | p := config.GetDefaultGlobals() 33 | globals = &p 34 | } 35 | 36 | config.SetGlobals(globals) 37 | 38 | fmt.Println("Seed:", int64(opts.Seed)) 39 | rand.Seed(int64(opts.Seed)) 40 | } 41 | -------------------------------------------------------------------------------- /config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "flag" 4 | 5 | type Options struct { 6 | ConfigFile string 7 | DumpConfig bool 8 | IsHeadless bool 9 | IsDebugging bool 10 | TrialCount int 11 | Seed int 12 | } 13 | 14 | func GetOptions() *Options { 15 | opts := Options{} 16 | 17 | flag.BoolVar(&opts.DumpConfig, "dump-config", false, "Dump the default config to stdout") 18 | flag.BoolVar(&opts.IsDebugging, "debug", false, "Run simulation and display debug statistics") 19 | flag.BoolVar(&opts.IsHeadless, "headless", false, "Run simulation without visualization") 20 | flag.IntVar(&opts.TrialCount, "trials", 1, "Number of trials to run") 21 | flag.IntVar(&opts.Seed, "seed", 0, "Set the random seed") 22 | flag.StringVar(&opts.ConfigFile, "config", "", "Config file in JSON format") 23 | 24 | flag.Parse() 25 | 26 | return &opts 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch test package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "test", 12 | "program": "${workspaceFolder}/test" 13 | }, 14 | { 15 | "name": "Debug Simulation", 16 | "type": "go", 17 | "request": "launch", 18 | "mode": "debug", 19 | "program": "${workspaceFolder}/main.go" 20 | }, 21 | { 22 | "name": "Debug Headless", 23 | "type": "go", 24 | "request": "launch", 25 | "mode": "debug", 26 | "program": "${workspaceFolder}/main.go", 27 | "args": [ 28 | "-headless" 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /settings/custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "grid_unit_size": 5, 3 | "grid_width": 1000, 4 | "grid_height": 800, 5 | "grid_units_wide": 200, 6 | "grid_units_high": 160, 7 | "screen_width": 1400, 8 | "screen_height": 800, 9 | "population_update_interval": 100, 10 | "chance_to_add_organism": 0.02, 11 | "chance_to_add_food_item": 0.09, 12 | "max_food_value": 100, 13 | "min_food_value": 2, 14 | "max_cycles_between_spawns": 100, 15 | "min_spawn_health": 1, 16 | "max_spawn_health_percent": 0.5, 17 | "min_chance_to_mutate_decision_tree": 0.01, 18 | "max_chance_to_mutate_decision_tree": 1, 19 | "max_organisms": 20000, 20 | "growth_factor": 0.5, 21 | "maximum_max_size": 100, 22 | "minimum_max_size": 10, 23 | "health_change_per_cycle": -0.001, 24 | "health_change_from_being_idle": 0.002, 25 | "health_change_from_turning": -0.01, 26 | "health_change_from_moving": -0.02, 27 | "health_change_from_eating_attempt": -0.01, 28 | "health_change_from_attacking": -0.05, 29 | "health_change_inflicted_by_attack": -0.5, 30 | "health_change_from_feeding": -0.05, 31 | "max_decision_tree_size": 16, 32 | "max_decision_trees": 5 33 | } 34 | -------------------------------------------------------------------------------- /decision/utils.go: -------------------------------------------------------------------------------- 1 | package decision 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | // CalcAndUpdateSize returns the total number of nodes descending from this root node (including itself) 8 | // Update each node's size value to avoid calculating this multiple times 9 | func (n *Node) CalcAndUpdateSize() int { 10 | if n.IsAction() { 11 | n.size = 1 12 | return 1 13 | } 14 | 15 | n.size = 1 + n.YesNode.CalcAndUpdateSize() + n.NoNode.CalcAndUpdateSize() 16 | return n.size 17 | } 18 | 19 | // GetRandomCondition returns a random Condition from the Conditions array 20 | func GetRandomCondition() Condition { 21 | return Conditions[rand.Intn(len(Conditions))] 22 | } 23 | 24 | // GetRandomAction returns a random Action from the Actions array 25 | func GetRandomAction() Action { 26 | return Actions[rand.Intn(len(Actions))] 27 | } 28 | 29 | // isAction returns true if the object passed in is an Action 30 | func isAction(v interface{}) bool { 31 | switch v.(type) { 32 | case Action: 33 | return true 34 | } 35 | return false 36 | } 37 | 38 | // isCondition returns true if the object passed in is a Condition 39 | func isCondition(v interface{}) bool { 40 | switch v.(type) { 41 | case Condition: 42 | return true 43 | } 44 | return false 45 | } 46 | -------------------------------------------------------------------------------- /organism/api.go: -------------------------------------------------------------------------------- 1 | package organism 2 | 3 | import ( 4 | "github.com/Zebbeni/protozoa/food" 5 | "github.com/Zebbeni/protozoa/utils" 6 | ) 7 | 8 | // FoodCheck is a true/false test to run on a given food Item 9 | type FoodCheck func(item *food.Item, exists bool) bool 10 | 11 | // OrgCheck is a true/false test to run on a given food Organism 12 | type OrgCheck func(item *Organism) bool 13 | 14 | // LookupAPI provides functions to look up items and organisms 15 | type LookupAPI interface { 16 | CheckFoodAtPoint(point utils.Point, checkFunc FoodCheck) bool 17 | CheckOrganismAtPoint(point utils.Point, checkFunc OrgCheck) bool 18 | GetFoodAtPoint(point utils.Point) (*food.Item, bool) 19 | GetPhAtPoint(point utils.Point) float64 20 | OrganismCount() int 21 | Cycle() int 22 | GetSelected() int 23 | } 24 | 25 | // ChangeAPI provides callback functions to make changes to the simulation 26 | type ChangeAPI interface { 27 | // AddFoodAtPoint requests adding some amount of food at a Point 28 | AddFoodAtPoint(point utils.Point, value int) 29 | // RemoveFoodAtPoint requests removing some amount of food at a Point 30 | RemoveFoodAtPoint(point utils.Point, value int) 31 | // AddPhChangeAtPoint adds a positive or negative value to the environment 32 | // pH at a given point, bounded by the min / max pH allowed by the config 33 | AddPhChangeAtPoint(point utils.Point, change float64) 34 | // AddOrganismUpdate adds a point to the update map of noteworthy locations 35 | // affected by organism activity 36 | AddOrganismUpdate(point utils.Point) 37 | } 38 | 39 | // API provides functions needed to lookup and make changes to world objects 40 | type API interface { 41 | LookupAPI 42 | ChangeAPI 43 | } 44 | -------------------------------------------------------------------------------- /settings/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial_food": 0, 3 | "initial_organisms": 800, 4 | "grid_unit_size": 5, 5 | "grid_width": 1000, 6 | "grid_height": 800, 7 | "grid_units_wide": 200, 8 | "grid_units_high": 160, 9 | "screen_width": 1400, 10 | "screen_height": 800, 11 | "population_update_interval": 20, 12 | "chance_to_add_food_item": 0.0, 13 | "max_food_value": 100, 14 | "min_food_value": 5, 15 | 16 | "max_cycles_between_spawns": 100, 17 | "min_spawn_health": 1, 18 | "max_spawn_health_percent": 0.5, 19 | 20 | "min_ph": 0.0, 21 | "max_ph": 10.0, 22 | "min_initial_ph": 1.5, 23 | "max_initial_ph": 8.5, 24 | "min_ideal_ph": 1.5, 25 | "max_ideal_ph": 8.5, 26 | "min_ph_tolerance": 0.5, 27 | "max_ph_tolerance": 1.0, 28 | "max_organism_ph_growth_effect": 0.05, 29 | "ph_diffuse_factor": 0.01, 30 | "ph_increment_to_display": 0.1, 31 | 32 | "use_pools": false, 33 | "pool_width": 10, 34 | "pool_height": 10, 35 | 36 | "initial_organism_decision_tree_mutations": 10, 37 | "min_chance_to_mutate_decision_tree": 0.01, 38 | "max_chance_to_mutate_decision_tree": 1.00, 39 | "max_decision_tree_size": 32, 40 | 41 | "max_organisms": 50000, 42 | "min_organisms": 20, 43 | "growth_factor": 0.5, 44 | "maximum_max_size": 100, 45 | "minimum_max_size": 20, 46 | "health_change_from_chemosynthesis": 0.02, 47 | "health_change_from_turning": -0.01, 48 | "health_change_from_eating_attempt": -0.01, 49 | "health_change_from_moving": -0.02, 50 | "health_change_from_attacking": -0.05, 51 | "health_change_inflicted_by_attack": -1.0, 52 | "health_change_from_feeding": -0.1, 53 | "health_change_per_decision_tree_node": -0.001, 54 | "health_change_per_unhealthy_ph": -0.5 55 | } -------------------------------------------------------------------------------- /manager/updates.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/Zebbeni/protozoa/utils" 7 | ) 8 | 9 | type UpdateManager struct { 10 | organismUpdates map[string]utils.Point 11 | phUpdates map[string]utils.Point 12 | foodUpdates map[string]utils.Point 13 | 14 | mutex sync.Mutex 15 | } 16 | 17 | func NewUpdateManager() *UpdateManager { 18 | m := &UpdateManager{} 19 | m.ClearMaps() 20 | return m 21 | } 22 | 23 | func (m *UpdateManager) ClearMaps() { 24 | m.mutex.Lock() 25 | m.organismUpdates = make(map[string]utils.Point) 26 | m.phUpdates = make(map[string]utils.Point) 27 | m.foodUpdates = make(map[string]utils.Point) 28 | m.mutex.Unlock() 29 | } 30 | 31 | func (m *UpdateManager) AddOrganismUpdate(p utils.Point) { 32 | m.mutex.Lock() 33 | m.organismUpdates[p.ToString()] = p 34 | m.mutex.Unlock() 35 | } 36 | 37 | // GetUpdatedOrganismPoints returns the full updated organism point map 38 | // (We should do this in a way that avoids sharing the actual map) 39 | func (m *UpdateManager) GetUpdatedOrganismPoints() map[string]utils.Point { 40 | return m.organismUpdates 41 | } 42 | 43 | func (m *UpdateManager) AddPhUpdate(p utils.Point) { 44 | m.mutex.Lock() 45 | m.phUpdates[p.ToString()] = p 46 | m.mutex.Unlock() 47 | } 48 | 49 | // GetUpdatedPhPoints returns the full updated ph point map 50 | // (We should do this in a way that avoids sharing the actual map) 51 | func (m *UpdateManager) GetUpdatedPhPoints() map[string]utils.Point { 52 | return m.phUpdates 53 | } 54 | 55 | func (m *UpdateManager) AddFoodUpdate(p utils.Point) { 56 | m.mutex.Lock() 57 | m.foodUpdates[p.ToString()] = p 58 | m.mutex.Unlock() 59 | } 60 | 61 | // GetUpdatedFoodPoints returns the full updated food point map 62 | // (We should do this in a way that avoids sharing the actual map) 63 | func (m *UpdateManager) GetUpdatedFoodPoints() map[string]utils.Point { 64 | return m.foodUpdates 65 | } 66 | -------------------------------------------------------------------------------- /ux/debug.go: -------------------------------------------------------------------------------- 1 | package ux 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "time" 7 | 8 | "github.com/Zebbeni/protozoa/simulation" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 12 | ) 13 | 14 | const ( 15 | debugWidth = 250 16 | debugHeight = 300 17 | ) 18 | 19 | type Debug struct { 20 | simulation *simulation.Simulation 21 | image *ebiten.Image 22 | 23 | renderTime time.Duration 24 | gridRenderTime time.Duration 25 | panelRenderTime time.Duration 26 | } 27 | 28 | func NewDebug(sim *simulation.Simulation) *Debug { 29 | return &Debug{ 30 | simulation: sim, 31 | } 32 | } 33 | 34 | func (d *Debug) render() *ebiten.Image { 35 | image := ebiten.NewImage(debugWidth, debugHeight) 36 | 37 | var m runtime.MemStats 38 | runtime.ReadMemStats(&m) 39 | // write info to screen 40 | info := fmt.Sprintf("FPS: %0.2f", ebiten.CurrentFPS()) 41 | info = fmt.Sprintf("%s\nAlloc: %v", info, m.Alloc/1024) 42 | info = fmt.Sprintf("%s\nTotalAlloc: %v", info, m.TotalAlloc/1024) 43 | info = fmt.Sprintf("%s\nSys: %v", info, m.Sys/1024) 44 | info = fmt.Sprintf("%s\nNumGC: %v", info, m.NumGC/1024) 45 | info = fmt.Sprintf("%s\nEnvironmentUpdate: %7s", info, d.simulation.EnvironmentUpdateTime) 46 | info = fmt.Sprintf("%s\nFoodUpdate: %10s", info, d.simulation.FoodUpdateTime) 47 | info = fmt.Sprintf("%s\nOrganismUpdate: %10s", info, d.simulation.OrganismUpdateTime) 48 | info = fmt.Sprintf("%s\n OrganismUpdateLoop: %10s", info, d.simulation.OrganismUpdateLoopTime) 49 | info = fmt.Sprintf("%s\n OrganismResolveLoop: %10s", info, d.simulation.OrganismResolveLoopTime) 50 | info = fmt.Sprintf("%s\nTotal Update: %10s", info, d.simulation.UpdateTime) 51 | info = fmt.Sprintf("%s\nRender Grid: %10s", info, d.gridRenderTime) 52 | info = fmt.Sprintf("%s\nRender Panel: %10s", info, d.panelRenderTime) 53 | info = fmt.Sprintf("%s\nTotal Render: %10s", info, d.renderTime) 54 | info = fmt.Sprintf("%s\nTotal: %10s", info, d.renderTime+d.simulation.UpdateTime) 55 | ebitenutil.DebugPrint(image, info) 56 | 57 | return image 58 | } 59 | -------------------------------------------------------------------------------- /runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | c "github.com/Zebbeni/protozoa/config" 9 | "github.com/Zebbeni/protozoa/resources" 10 | "github.com/Zebbeni/protozoa/simulation" 11 | "github.com/Zebbeni/protozoa/ux" 12 | "github.com/hajimehoshi/ebiten/v2" 13 | ) 14 | 15 | type Runner struct { 16 | sim *simulation.Simulation 17 | ui *ux.Interface 18 | 19 | pressedKeys map[ebiten.Key]bool 20 | } 21 | 22 | func (r *Runner) Update() error { 23 | r.handleUserInput() 24 | r.sim.Update() 25 | r.updateSelected() 26 | return nil 27 | } 28 | 29 | func (r *Runner) handleUserInput() { 30 | r.ui.HandleUserInput() 31 | } 32 | 33 | func (r *Runner) updateSelected() { 34 | r.ui.UpdateSelected() 35 | } 36 | 37 | func (r *Runner) Draw(screen *ebiten.Image) { 38 | r.ui.Render(screen) 39 | r.sim.ClearUpdatedPoints() 40 | } 41 | 42 | func (r *Runner) Layout(_, _ int) (int, int) { 43 | return c.ScreenWidth(), c.ScreenHeight() 44 | } 45 | 46 | func RunSimulation(opts *c.Options) { 47 | resources.Init() 48 | 49 | if opts.IsHeadless { 50 | sumAllCycles := 0 51 | for count := 0; count < opts.TrialCount; count++ { 52 | sim := simulation.NewSimulation(opts) 53 | start := time.Now() 54 | for !sim.IsDone() { 55 | sim.Update() 56 | if sim.Cycle()%100 == 0 { 57 | fmt.Printf("\nCycle: %6d Organisms: %d AvgPh: %2.2f", sim.Cycle(), sim.OrganismCount(), sim.AveragePh()) 58 | } 59 | } 60 | sumAllCycles += sim.Cycle() 61 | elapsed := time.Since(start) 62 | fmt.Printf("\nTotal runtime for simulation %d: %s, cycles: %d\n", count, elapsed, sim.Cycle()) 63 | } 64 | avgCycles := sumAllCycles / opts.TrialCount 65 | fmt.Printf("\nAverage number of cycles to reach 5000: %d\n", avgCycles) 66 | } else { 67 | sim := simulation.NewSimulation(opts) 68 | 69 | ui := ux.NewInterface(sim) 70 | gameRunner := &Runner{ 71 | sim: sim, 72 | ui: ui, 73 | pressedKeys: map[ebiten.Key]bool{}, 74 | } 75 | 76 | ebiten.SetWindowResizable(true) 77 | ebiten.SetScreenClearedEveryFrame(false) 78 | if err := ebiten.RunGame(gameRunner); err != nil { 79 | log.Fatal(err) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /manager/requests.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "github.com/Zebbeni/protozoa/food" 5 | "github.com/Zebbeni/protozoa/utils" 6 | "sync" 7 | ) 8 | 9 | // RequestManager manages access maps that keep track of overlapping or 10 | // conflicting requests placed by organisms due to concurrent action updates 11 | type RequestManager struct { 12 | positionRequests map[string]int // the lowest id of an organism requesting to move or spawn at a point 13 | foodRequests map[string]food.Item // the amount of food eaten at a given point 14 | healthEffectRequests map[string]float64 // the total damage + healing effects at a given location 15 | 16 | mutex sync.Mutex 17 | } 18 | 19 | func (m *RequestManager) ClearMaps() { 20 | m.mutex.Lock() 21 | defer m.mutex.Unlock() 22 | 23 | m.positionRequests = make(map[string]int) 24 | m.foodRequests = make(map[string]food.Item) 25 | m.healthEffectRequests = make(map[string]float64) 26 | } 27 | 28 | func (m *RequestManager) GetPositionRequest(p utils.Point) int { 29 | m.mutex.Lock() 30 | defer m.mutex.Unlock() 31 | 32 | return m.positionRequests[p.ToString()] 33 | } 34 | 35 | func (m *RequestManager) GetFoodRequests(p utils.Point) food.Item { 36 | m.mutex.Lock() 37 | defer m.mutex.Unlock() 38 | 39 | return m.foodRequests[p.ToString()] 40 | } 41 | 42 | func (m *RequestManager) GetHealthEffects(p utils.Point) float64 { 43 | m.mutex.Lock() 44 | defer m.mutex.Unlock() 45 | 46 | return m.healthEffectRequests[p.ToString()] 47 | } 48 | 49 | func (m *RequestManager) AddPositionRequest(p utils.Point, id int) { 50 | pString := p.ToString() 51 | 52 | m.mutex.Lock() 53 | if id > m.positionRequests[pString] { 54 | m.positionRequests[pString] = id 55 | } 56 | m.mutex.Unlock() 57 | } 58 | 59 | func (m *RequestManager) AddFoodRequest(p utils.Point, value int) { 60 | pString := p.ToString() 61 | m.mutex.Lock() 62 | if item, ok := m.foodRequests[pString]; ok { 63 | value += item.Value 64 | } 65 | m.foodRequests[pString] = food.Item{Point: p, Value: value} 66 | m.mutex.Unlock() 67 | } 68 | 69 | func (m *RequestManager) AddHealthEffectRequest(p utils.Point, v float64) { 70 | pString := p.ToString() 71 | m.mutex.Lock() 72 | m.healthEffectRequests[pString] += v 73 | m.mutex.Unlock() 74 | } 75 | -------------------------------------------------------------------------------- /decision/tree.go: -------------------------------------------------------------------------------- 1 | package decision 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/Zebbeni/protozoa/config" 7 | ) 8 | 9 | // Tree is a Node with info to track its success as a top-level decision tree 10 | type Tree struct { 11 | ID string 12 | *Node 13 | } 14 | 15 | // TreeFromAction returns a simple decision Tree from an Action type 16 | func TreeFromAction(action Action) *Tree { 17 | tree := &Tree{ 18 | Node: NodeFromAction(action), 19 | } 20 | tree.ID = tree.Serialize() 21 | return tree 22 | } 23 | 24 | // CopyTree returns a new, identical decision tree 25 | // Includes current stats as well if copyHistory=true 26 | func (t *Tree) CopyTree() *Tree { 27 | tree := &Tree{ 28 | ID: t.ID, 29 | Node: t.Node.CopyNode(), 30 | } 31 | return tree 32 | } 33 | 34 | // MutateTree copies a root Tree, makes changes to the full tree, and returns 35 | func MutateTree(original *Tree) *Tree { 36 | tree := original.CopyTree() 37 | tree.mutate() 38 | return tree 39 | } 40 | 41 | // mutate randomly mutates a single node of a tree. This function 42 | // should only be called on root tree nodes because it uses the tree size. 43 | func (t *Tree) mutate() { 44 | // pick a random t anywhere in the decision tree 45 | allSubNodes := t.getNodes() 46 | node := allSubNodes[rand.Intn(len(allSubNodes))] 47 | 48 | maxTreeSize := config.MaxDecisionTreeSize() 49 | 50 | if node.IsAction() { 51 | if rand.Intn(2) == 0 && t.size < maxTreeSize-1 { 52 | // convert action to condition + 2 actions 53 | originalAction := node.NodeType.(Action) 54 | node.NodeType = GetRandomCondition() 55 | if rand.Intn(2) == 0 { 56 | node.YesNode = NodeFromAction(GetRandomAction()) 57 | node.NoNode = NodeFromAction(originalAction) 58 | } else { 59 | node.YesNode = NodeFromAction(originalAction) 60 | node.NoNode = NodeFromAction(GetRandomAction()) 61 | } 62 | } else { 63 | // change action type 64 | node.NodeType = GetRandomAction() 65 | } 66 | } else { 67 | if rand.Intn(2) == 0 { 68 | // convert condition to action (simplify) 69 | node.NodeType = GetRandomAction() 70 | node.YesNode = nil 71 | node.NoNode = nil 72 | } else { 73 | // change condition type 74 | node.NodeType = GetRandomCondition() 75 | } 76 | } 77 | 78 | t.size = t.CalcAndUpdateSize() 79 | t.ResetUsedLastCycle() 80 | } 81 | 82 | func (t *Tree) Size() int { 83 | return t.size 84 | } 85 | 86 | // Print prints the full tree structure 87 | func (t *Tree) Print() string { 88 | return t.print("", true, false) 89 | } 90 | -------------------------------------------------------------------------------- /decision/constants.go: -------------------------------------------------------------------------------- 1 | package decision 2 | 3 | // Action is the custom type for all Organism actions 4 | type Action int 5 | 6 | // Condition is the custom type for all Organism conditions 7 | type Condition int 8 | 9 | // Define all possible actions for Organism 10 | const ( 11 | ActAttack Action = iota 12 | ActFeed 13 | ActEat 14 | ActChemosynthesis 15 | ActMove 16 | ActTurnLeft 17 | ActTurnRight 18 | ActSpawn 19 | CanMove Condition = iota 20 | IsFoodAhead 21 | IsFoodLeft 22 | IsFoodRight 23 | IsOrganismAhead 24 | IsBiggerOrganismAhead 25 | IsRelatedOrganismAhead 26 | IsOrganismLeft 27 | IsRelatedOrganismLeft 28 | IsOrganismRight 29 | IsRelatedOrganismRight 30 | IsHealthAboveFiftyPercent 31 | IsHealthyPhHere 32 | IsHealthierPhAhead 33 | //IsRandomFiftyPercent 34 | ) 35 | 36 | // Define slices 37 | var ( 38 | Actions = [...]Action{ 39 | ActAttack, 40 | ActFeed, 41 | ActEat, 42 | ActChemosynthesis, 43 | ActMove, 44 | ActTurnLeft, 45 | ActTurnRight, 46 | // ActSpawn <-- Leave this out since it's not something we want organisms to 'choose' to do 47 | } 48 | Conditions = [...]Condition{ 49 | CanMove, 50 | IsFoodAhead, 51 | IsFoodLeft, 52 | IsFoodRight, 53 | IsOrganismAhead, 54 | IsBiggerOrganismAhead, 55 | IsRelatedOrganismAhead, 56 | IsOrganismLeft, 57 | IsRelatedOrganismLeft, 58 | IsOrganismRight, 59 | IsRelatedOrganismRight, 60 | IsHealthAboveFiftyPercent, 61 | IsHealthyPhHere, 62 | IsHealthierPhAhead, 63 | //IsRandomFiftyPercent, 64 | } 65 | Map = map[interface{}]string{ 66 | ActAttack: "Attack", 67 | ActFeed: "Feed", 68 | ActEat: "Eat", 69 | ActChemosynthesis: "Chemosynthesis", 70 | ActMove: "Move Ahead", 71 | ActTurnLeft: "Turn Left", 72 | ActTurnRight: "Turn Right", 73 | ActSpawn: "Spawn", 74 | CanMove: "If Can Move Ahead", 75 | IsFoodAhead: "If Food Ahead", 76 | IsFoodLeft: "If Food Left", 77 | IsFoodRight: "If Food Right", 78 | IsOrganismAhead: "If Organism Ahead", 79 | IsBiggerOrganismAhead: "If Bigger Organism Ahead", 80 | IsRelatedOrganismAhead: "If Related Organism Ahead", 81 | IsOrganismLeft: "If Organism Left", 82 | IsRelatedOrganismLeft: "If Related Organism Left", 83 | IsOrganismRight: "If Organism Right", 84 | IsRelatedOrganismRight: "If Related Organism Right", 85 | IsHealthAboveFiftyPercent: "IsHealthAboveFiftyPercent", 86 | IsHealthyPhHere: "IsHealthyPhHere", 87 | IsHealthierPhAhead: "IsHealthierPhAhead", 88 | //IsRandomFiftyPercent: "IsRandomFiftyPercent", 89 | } 90 | ) 91 | -------------------------------------------------------------------------------- /utils/geometry.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | c "github.com/Zebbeni/protozoa/config" 8 | ) 9 | 10 | // Point contains simple X and Y coordinates for a point in space 11 | // also usable with addition / subtraction as a directional unit vector 12 | type Point struct { 13 | X, Y int 14 | } 15 | 16 | func (p Point) String() string { 17 | return fmt.Sprintf("(%d, %d)", p.X, p.Y) 18 | } 19 | 20 | var ( 21 | directionUp = Point{X: 0, Y: -1} 22 | directionRight = Point{X: +1, Y: 0} 23 | directionDown = Point{X: 0, Y: +1} 24 | directionLeft = Point{X: -1, Y: 0} 25 | // Directions is a list of all possible directions 26 | // to travel on the simulation grid 27 | Directions = [...]Point{ 28 | directionUp, 29 | directionRight, 30 | directionDown, 31 | directionLeft, 32 | } 33 | ) 34 | 35 | // GetRandomPoint returns a random point somewhere on the simulation grid 36 | func GetRandomPoint(width, height int) Point { 37 | return Point{ 38 | X: rand.Intn(width), 39 | Y: rand.Intn(height), 40 | } 41 | } 42 | 43 | // GetRandomDirection returns a point representing a random direction 44 | func GetRandomDirection() Point { 45 | return Directions[rand.Intn(len(Directions))] 46 | } 47 | 48 | // Add add a given Point and returns the result 49 | func (p Point) Add(toAdd Point) Point { 50 | return Point{X: p.X + toAdd.X, Y: p.Y + toAdd.Y}.Wrap() 51 | } 52 | 53 | // Times multiplies a given value and returns the result 54 | func (p *Point) Times(toMultiply int) Point { 55 | return Point{ 56 | X: p.X * toMultiply, 57 | Y: p.Y * toMultiply, 58 | } 59 | } 60 | 61 | // Wrap returns a point value after wrapping it around the grid 62 | func (p Point) Wrap() Point { 63 | return Point{ 64 | X: (p.X + c.GridUnitsWide()) % c.GridUnitsWide(), 65 | Y: (p.Y + c.GridUnitsHigh()) % c.GridUnitsHigh(), 66 | } 67 | } 68 | 69 | // IsWall returns true if the given point is on a pool border 70 | func (p *Point) IsWall() bool { 71 | return IsWall(p.X, p.Y) 72 | } 73 | 74 | // IsWall returns true if some given coordinates are on a pool border, making 75 | // sure to allow movement through 'gates' in the center of each wall. 76 | func IsWall(x, y int) bool { 77 | if c.UsePools() == false { 78 | return false 79 | } 80 | 81 | if x%c.PoolWidth() == 0 && (y+(c.PoolHeight()/2))%c.PoolHeight() != 0 { 82 | return true 83 | } 84 | if y%c.PoolHeight() == 0 && (x+(c.PoolWidth()/2))%c.PoolWidth() != 0 { 85 | return true 86 | } 87 | return false 88 | } 89 | 90 | // ToString returns a Point's values as the string, ", " 91 | func (p *Point) ToString() string { 92 | return fmt.Sprintf("%d,%d", p.X, p.Y) 93 | } 94 | 95 | // Right returns the direction to the right of the current direction d 96 | func (p Point) Right() (right Point) { 97 | switch p { 98 | case directionUp: 99 | right = directionRight 100 | break 101 | case directionRight: 102 | right = directionDown 103 | break 104 | case directionDown: 105 | right = directionLeft 106 | break 107 | case directionLeft: 108 | right = directionUp 109 | break 110 | } 111 | return 112 | } 113 | 114 | // Left returns the direction to the right of the current direction d 115 | func (p Point) Left() (left Point) { 116 | switch p { 117 | case directionUp: 118 | left = directionLeft 119 | break 120 | case directionRight: 121 | left = directionUp 122 | break 123 | case directionDown: 124 | left = directionRight 125 | break 126 | case directionLeft: 127 | left = directionDown 128 | break 129 | } 130 | return 131 | } 132 | -------------------------------------------------------------------------------- /decision/node.go: -------------------------------------------------------------------------------- 1 | package decision 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | // Node contains an Action or Condition NodeType and (if a Condition), child 10 | // references for its conditional branches 11 | type Node struct { 12 | NodeType interface{} 13 | InDecisionTree, UsedLastCycle bool 14 | YesNode, NoNode *Node 15 | size int 16 | 17 | mutex sync.Mutex 18 | } 19 | 20 | // NodeFromAction creates a simple Node object from an Action type 21 | func NodeFromAction(action Action) *Node { 22 | return &Node{ 23 | NodeType: action, 24 | size: 1, 25 | } 26 | } 27 | 28 | // IsAction returns true if Tree's type is Action (false if Condition) 29 | func (n *Node) IsAction() bool { 30 | return isAction(n.NodeType) 31 | } 32 | 33 | // IsCondition returns true if Tree's type is Action (false if Condition) 34 | func (n *Node) IsCondition() bool { 35 | return isCondition(n.NodeType) 36 | } 37 | 38 | // CopyNode returns a new Node with the same structure as the original 39 | func (n Node) CopyNode() *Node { 40 | copy := &Node{ 41 | NodeType: n.NodeType, 42 | UsedLastCycle: n.UsedLastCycle, 43 | size: n.size, 44 | } 45 | if n.IsAction() { 46 | return copy 47 | } 48 | copy.YesNode = n.YesNode.CopyNode() 49 | copy.NoNode = n.NoNode.CopyNode() 50 | return copy 51 | } 52 | 53 | // SetUsedInCurrentTree sets whether this Node is contained in a 54 | // currently-used decision tree 55 | func (n *Node) SetUsedInCurrentTree(isUsing bool) { 56 | n.InDecisionTree = isUsing 57 | if n.IsCondition() { 58 | n.YesNode.SetUsedInCurrentTree(isUsing) 59 | n.NoNode.SetUsedInCurrentTree(isUsing) 60 | } 61 | } 62 | 63 | // ResetUsedLastCycle triggers this Node (and any previously-used child Nodes) 64 | // to set UsedLastCycle to false 65 | func (n *Node) ResetUsedLastCycle() { 66 | n.UsedLastCycle = false 67 | if n.IsCondition() { 68 | if n.YesNode.UsedLastCycle { 69 | n.YesNode.ResetUsedLastCycle() 70 | } else { 71 | n.NoNode.ResetUsedLastCycle() 72 | } 73 | } 74 | } 75 | 76 | // Serialize generates and returns a string representing a Node's 77 | // full Tree structure. 78 | // 79 | // Recursively walks through the Node tree to accumulate a string representing 80 | // itself and all its children 81 | func (n *Node) Serialize() string { 82 | var buffer bytes.Buffer 83 | nodeTypeString := fmt.Sprintf("%02d", n.NodeType) 84 | buffer.WriteString(nodeTypeString) 85 | if n.IsCondition() { 86 | buffer.WriteString(n.YesNode.Serialize()) 87 | buffer.WriteString(n.NoNode.Serialize()) 88 | } 89 | return buffer.String() 90 | } 91 | 92 | // getNodes returns a list of all nodes in a tree starting with the given root 93 | func (n *Node) getNodes() (nodes []*Node) { 94 | nodes = make([]*Node, 0, n.size) 95 | nodes = append(nodes, n) 96 | if n.IsAction() { 97 | return 98 | } 99 | 100 | nodes = append(nodes, n.YesNode.getNodes()...) 101 | nodes = append(nodes, n.NoNode.getNodes()...) 102 | return 103 | } 104 | 105 | func (n *Node) print(indent string, first, last bool) string { 106 | toPrint := indent 107 | newIndent := indent 108 | if first { 109 | toPrint = fmt.Sprintf("%s", toPrint) 110 | } else if last { 111 | toPrint = fmt.Sprintf("%s└─", toPrint) 112 | newIndent = fmt.Sprintf("%s ", newIndent) 113 | } else { 114 | toPrint = fmt.Sprintf("%s├─", toPrint) 115 | newIndent = fmt.Sprintf("%s│ ", newIndent) 116 | } 117 | if n.UsedLastCycle { 118 | toPrint = fmt.Sprintf("%s%s ◀◀\n", toPrint, Map[n.NodeType]) 119 | } else { 120 | toPrint = fmt.Sprintf("%s%s\n", toPrint, Map[n.NodeType]) 121 | } 122 | if n.IsCondition() { 123 | toPrint = fmt.Sprintf("%s%s", toPrint, n.YesNode.print(newIndent, false, false)) 124 | toPrint = fmt.Sprintf("%s%s", toPrint, n.NoNode.print(newIndent, false, true)) 125 | } 126 | return toPrint 127 | } 128 | -------------------------------------------------------------------------------- /resources/resources.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/Zebbeni/protozoa/config" 12 | 13 | "github.com/hajimehoshi/ebiten/v2" 14 | "golang.org/x/image/font" 15 | "golang.org/x/image/font/opentype" 16 | ) 17 | 18 | const ( 19 | dpi = 72 20 | ) 21 | 22 | var ( 23 | // FontInversionz40 is a size 50 Inversionz font face 24 | FontInversionz40 font.Face 25 | // FontSourceCodePro12 is a size 12 SourceCodePro (Regular) font face 26 | FontSourceCodePro12 font.Face 27 | // FontSourceCodePro10 is a size 10 SourceCodePro (Regular) font face 28 | FontSourceCodePro10 font.Face 29 | // FontSourceCodePro8 is a size 8 SourceCodePro (Regular) font face 30 | FontSourceCodePro8 font.Face 31 | 32 | // PlayButton is a 30x30 image 33 | PlayButton *ebiten.Image 34 | // PauseButton is a 30x30 image 35 | PauseButton *ebiten.Image 36 | 37 | // SquareSmall is an image to render for small organisms and food 38 | SquareSmall *ebiten.Image 39 | // SquareMedium is an image to render for medium organisms and food 40 | SquareMedium *ebiten.Image 41 | // SquareLarge is an image to render for large organisms and food 42 | SquareLarge *ebiten.Image 43 | // SquareFill is an image to render for totally filled grid spaces 44 | SquareFill *ebiten.Image 45 | // SquareBox is an image to render for walls 46 | SquareBox *ebiten.Image 47 | ) 48 | 49 | // Init loads all fonts and images to be used in the UI 50 | func Init() { 51 | initFonts() 52 | initImages() 53 | } 54 | 55 | func initFonts() { 56 | inversionz := loadFont("resources/fonts/Inversionz.ttf") 57 | FontInversionz40 = fontFace(inversionz, 40) 58 | sourceCode := loadFont("resources/fonts/SourceCodePro-Regular.ttf") 59 | FontSourceCodePro12 = fontFace(sourceCode, 12) 60 | FontSourceCodePro10 = fontFace(sourceCode, 10) 61 | FontSourceCodePro8 = fontFace(sourceCode, 8) 62 | } 63 | 64 | func initImages() { 65 | // Panel Images 66 | PlayButton = loadImage("resources/images/play_button.png") 67 | PauseButton = loadImage("resources/images/pause_button.png") 68 | 69 | var dir string 70 | switch config.GridUnitSize() { 71 | case 4: 72 | dir = "4x4" 73 | break 74 | case 5: 75 | dir = "5x5" 76 | break 77 | case 8: 78 | dir = "8x8" 79 | break 80 | default: 81 | panic(fmt.Sprintf("Unsupported grid unit size: %d", config.GridUnitSize())) 82 | } 83 | SquareSmall = loadImage(fmt.Sprintf("resources/images/grid/%s/square_small.png", dir)) 84 | SquareMedium = loadImage(fmt.Sprintf("resources/images/grid/%s/square_large.png", dir)) 85 | SquareLarge = loadImage(fmt.Sprintf("resources/images/grid/%s/square_large.png", dir)) 86 | SquareFill = loadImage(fmt.Sprintf("resources/images/grid/%s/square_fill.png", dir)) 87 | SquareBox = loadImage(fmt.Sprintf("resources/images/grid/%s/square_box.png", dir)) 88 | } 89 | 90 | func loadImage(path string) *ebiten.Image { 91 | filepath, err := filepath.Abs(path) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | reader, err := os.Open(filepath) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | img, err := png.Decode(reader) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | ebitenImg := ebiten.NewImageFromImage(img) 104 | return ebitenImg 105 | } 106 | 107 | func loadFont(path string) *opentype.Font { 108 | filepath, err := filepath.Abs(path) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | fontData, err := ioutil.ReadFile(filepath) 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | tt, err := opentype.Parse(fontData) 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | return tt 121 | } 122 | 123 | func fontFace(openFont *opentype.Font, size float64) font.Face { 124 | face, err := opentype.NewFace(openFont, &opentype.FaceOptions{ 125 | Size: size, 126 | DPI: dpi, 127 | Hinting: font.HintingFull, 128 | }) 129 | if err != nil { 130 | log.Fatal(err) 131 | } 132 | return face 133 | } 134 | -------------------------------------------------------------------------------- /manager/food.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "sync" 7 | 8 | "github.com/Zebbeni/protozoa/config" 9 | "github.com/Zebbeni/protozoa/food" 10 | "github.com/Zebbeni/protozoa/utils" 11 | ) 12 | 13 | // FoodManager contains 2D array of all food values 14 | type FoodManager struct { 15 | api food.API 16 | Items map[string]*food.Item 17 | isInitialized bool 18 | 19 | mutex sync.RWMutex 20 | } 21 | 22 | // NewFoodManager initializes a new foodItem map of MinFood 23 | func NewFoodManager(api food.API) *FoodManager { 24 | m := &FoodManager{ 25 | api: api, 26 | Items: make(map[string]*food.Item), 27 | isInitialized: false, 28 | } 29 | m.InitializeFood(config.InitialFood()) 30 | return m 31 | } 32 | 33 | func (m *FoodManager) InitializeFood(count int) { 34 | for i := 0; i < count; i++ { 35 | m.AddRandomFoodItem() 36 | } 37 | } 38 | 39 | // Update is called on every cycle and adds new FoodItems at a constant rate 40 | func (m *FoodManager) Update() { 41 | if rand.Float64() < config.ChanceToAddFoodItem() { 42 | m.AddRandomFoodItem() 43 | } 44 | return 45 | } 46 | 47 | // FoodCount returns a count of all food items in the FoodManager map 48 | func (m *FoodManager) FoodCount() int { 49 | return len(m.Items) 50 | } 51 | 52 | // AddRandomFoodItem attempts to add a FoodItem object to a random location 53 | // Gives up if first attempt to place food fails. 54 | func (m *FoodManager) AddRandomFoodItem() { 55 | x := rand.Intn(config.GridUnitsWide()) 56 | y := rand.Intn(config.GridUnitsHigh()) 57 | value := rand.Intn(config.MaxFoodValue()) 58 | point := utils.Point{X: x, Y: y} 59 | m.addFood(point, value) 60 | } 61 | 62 | // AddFoodAtPoint adds a foodItem with a given value at a given location if not 63 | // occupied, or adds food to the existing food item there (up to maximum allowed) 64 | func (m *FoodManager) AddFoodAtPoint(point utils.Point, value int) { 65 | m.addFood(point, value) 66 | } 67 | 68 | // RemoveFoodAtPoint subtracts a given value from the Item at a given point. 69 | // If value is more than the current food value, remove foodItem from the map 70 | func (m *FoodManager) RemoveFoodAtPoint(point utils.Point, value int) { 71 | m.removeFood(point, value) 72 | } 73 | 74 | // GetFoodAtPoint returns the FoodItem value at a given point (nil if none found) 75 | func (m *FoodManager) GetFoodAtPoint(point utils.Point) (*food.Item, bool) { 76 | return m.getFood(point) 77 | } 78 | 79 | // GetFoodItems returns the current list of food items 80 | func (m *FoodManager) GetFoodItems() map[string]*food.Item { 81 | return m.Items 82 | } 83 | 84 | func (m *FoodManager) removeFood(point utils.Point, value int) { 85 | if value <= 0 || point.IsWall() { 86 | return 87 | } 88 | 89 | pointString := point.ToString() 90 | 91 | m.mutex.RLock() 92 | item, exists := m.Items[pointString] 93 | m.mutex.RUnlock() 94 | 95 | if !exists { 96 | return 97 | } 98 | 99 | item.Value -= value 100 | if item.Value <= config.MinFoodValue() { 101 | m.mutex.Lock() 102 | delete(m.Items, pointString) 103 | m.mutex.Unlock() 104 | } 105 | 106 | m.addUpdatedPoint(point) 107 | } 108 | 109 | func (m *FoodManager) addFood(point utils.Point, value int) { 110 | if value <= 0 || point.IsWall() { 111 | return 112 | } 113 | 114 | pointString := point.ToString() 115 | 116 | m.mutex.Lock() 117 | item, exists := m.Items[pointString] 118 | if exists { 119 | value += item.Value 120 | } 121 | value = int(math.Min(math.Max(0.0, float64(value)), float64(config.MaxFoodValue()))) 122 | m.Items[pointString] = food.NewItem(point, value) 123 | m.mutex.Unlock() 124 | 125 | m.addUpdatedPoint(point) 126 | } 127 | 128 | func (m *FoodManager) getFood(point utils.Point) (*food.Item, bool) { 129 | m.mutex.RLock() 130 | item, found := m.Items[point.ToString()] 131 | m.mutex.RUnlock() 132 | 133 | return item, found 134 | } 135 | 136 | func (m *FoodManager) addUpdatedPoint(point utils.Point) { 137 | m.api.AddFoodUpdate(point) 138 | } 139 | -------------------------------------------------------------------------------- /ux/interface.go: -------------------------------------------------------------------------------- 1 | package ux 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/hajimehoshi/ebiten/v2/inpututil" 8 | 9 | "github.com/Zebbeni/protozoa/config" 10 | "github.com/Zebbeni/protozoa/organism" 11 | "github.com/Zebbeni/protozoa/simulation" 12 | "github.com/Zebbeni/protozoa/utils" 13 | ) 14 | 15 | type Interface struct { 16 | simulation *simulation.Simulation 17 | selection *organism.Info 18 | 19 | grid *Grid 20 | panel *Panel 21 | debug *Debug 22 | 23 | gridOptions *ebiten.DrawImageOptions 24 | panelOptions *ebiten.DrawImageOptions 25 | debugOptions *ebiten.DrawImageOptions 26 | } 27 | 28 | func NewInterface(sim *simulation.Simulation) *Interface { 29 | i := &Interface{ 30 | simulation: sim, 31 | grid: NewGrid(sim), 32 | panel: NewPanel(sim), 33 | gridOptions: &ebiten.DrawImageOptions{}, 34 | panelOptions: &ebiten.DrawImageOptions{}, 35 | } 36 | i.gridOptions.GeoM.Translate(panelWidth, 0) 37 | 38 | i.debug = NewDebug(sim) 39 | i.debugOptions = &ebiten.DrawImageOptions{} 40 | i.debugOptions.GeoM.Translate(panelWidth, 0) 41 | return i 42 | } 43 | 44 | func (i *Interface) Render(screen *ebiten.Image) { 45 | screen.Clear() 46 | 47 | start := time.Now() 48 | 49 | i.renderGrid(screen) 50 | i.renderPanel(screen) 51 | 52 | i.debug.renderTime = time.Since(start) 53 | if i.simulation.IsDebug() { 54 | debugImage := i.debug.render() 55 | screen.DrawImage(debugImage, i.debugOptions) 56 | } 57 | } 58 | 59 | func (i *Interface) HandleUserInput() { 60 | i.handleKeyboard() 61 | i.handleMouse() 62 | } 63 | 64 | func (i *Interface) handleKeyboard() { 65 | if inpututil.IsKeyJustReleased(ebiten.KeySpace) { 66 | i.simulation.Pause(!i.simulation.IsPaused()) 67 | } 68 | if inpututil.IsKeyJustReleased(ebiten.KeyM) { 69 | i.grid.ChangeViewMode() 70 | } 71 | if inpututil.IsKeyJustReleased(ebiten.KeyO) { 72 | i.grid.UpdateAutoSelect() 73 | } 74 | if inpututil.IsKeyJustReleased(ebiten.KeyD) { 75 | i.simulation.ToggleDebug() 76 | } 77 | } 78 | 79 | func (i *Interface) UpdateSelected() { 80 | id := -1 81 | switch i.grid.selectMode { 82 | case selectOldest: 83 | id = i.simulation.GetOldestId() 84 | case selectMostChildren: 85 | id = i.simulation.GetMostChildrenId() 86 | case selectMostTraveled: 87 | id = i.simulation.GetMostTraveledId() 88 | default: 89 | return 90 | } 91 | i.simulation.Select(id) 92 | } 93 | 94 | // eventually let's implement a more comprehensive event handler system 95 | // but for right now, when the grid is the only thing we're using with mouse 96 | // events, I think this is fine. 97 | func (i *Interface) handleMouse() { 98 | i.handleMouseHover() 99 | 100 | if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { 101 | i.handleLeftClick() 102 | } 103 | } 104 | 105 | func (i *Interface) handleMouseHover() { 106 | gridLocation, onGrid := i.getMouseGridLocation() 107 | i.grid.MouseHover(gridLocation, onGrid) 108 | } 109 | 110 | func (i *Interface) handleLeftClick() { 111 | if selectedPoint, onGrid := i.getMouseGridLocation(); onGrid { 112 | i.grid.SetManualSelection() 113 | if info := i.simulation.GetOrganismInfoAtPoint(selectedPoint); info != nil { 114 | i.simulation.Select(info.ID) 115 | } else { 116 | i.simulation.Select(-1) 117 | } 118 | } 119 | } 120 | 121 | func (i *Interface) renderGrid(screen *ebiten.Image) { 122 | start := time.Now() 123 | gridImage := i.grid.Render() 124 | screen.DrawImage(gridImage, i.gridOptions) 125 | i.debug.gridRenderTime = time.Since(start) 126 | } 127 | 128 | func (i *Interface) renderPanel(screen *ebiten.Image) { 129 | start := time.Now() 130 | panelImage := i.panel.Render() 131 | screen.DrawImage(panelImage, i.panelOptions) 132 | i.debug.panelRenderTime = time.Since(start) 133 | } 134 | 135 | // getMouseGridLocation returns the mouse's point on the grid along with a 136 | // boolean telling us if the point is within the grid bounds 137 | func (i *Interface) getMouseGridLocation() (utils.Point, bool) { 138 | mouseX, mouseY := ebiten.CursorPosition() 139 | relativeGridX := mouseX - panelWidth 140 | relativeGridY := mouseY 141 | gridX := relativeGridX / config.GridUnitSize() 142 | gridY := relativeGridY / config.GridUnitSize() 143 | gridW := config.GridUnitsWide() 144 | gridH := config.GridUnitsHigh() 145 | onGrid := gridX >= 0 && gridY >= 0 && gridX < gridW && gridY < gridH 146 | return utils.Point{X: gridX, Y: gridY}, onGrid 147 | } 148 | -------------------------------------------------------------------------------- /ux/panel.go: -------------------------------------------------------------------------------- 1 | package ux 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 9 | "github.com/hajimehoshi/ebiten/v2/text" 10 | 11 | r "github.com/Zebbeni/protozoa/resources" 12 | s "github.com/Zebbeni/protozoa/simulation" 13 | ) 14 | 15 | const ( 16 | padding = 15 17 | panelWidth = 400 18 | panelHeight = 1000 19 | 20 | titleXOffset = padding 21 | titleYOffset = padding 22 | playXOffset = padding 23 | playYOffset = 0 24 | 25 | statsXOffset = padding 26 | statsYOffset = 69 27 | 28 | selectedXOffset = padding 29 | selectedYOffset = 300 30 | 31 | graphXOffset = padding 32 | graphYOffset = 130 33 | graphWidth = 370 34 | graphHeight = 120 35 | ) 36 | 37 | type Panel struct { 38 | simulation *s.Simulation 39 | previousPanelImage *ebiten.Image 40 | graph *Graph 41 | } 42 | 43 | func NewPanel(sim *s.Simulation) *Panel { 44 | return &Panel{ 45 | simulation: sim, 46 | graph: NewGraph(sim), 47 | } 48 | } 49 | 50 | func (p *Panel) Render() *ebiten.Image { 51 | panelImage := ebiten.NewImage(panelWidth, panelHeight) 52 | 53 | if p.shouldRefresh() { 54 | p.renderDividingLine(panelImage) 55 | p.renderTitle(panelImage) 56 | p.renderKeyBindingText(panelImage) 57 | p.renderStats(panelImage) 58 | p.renderGraph(panelImage) 59 | p.renderSelected(panelImage) 60 | 61 | p.previousPanelImage = ebiten.NewImage(panelWidth, panelHeight) 62 | p.previousPanelImage.DrawImage(panelImage, nil) 63 | } else { 64 | panelImage.DrawImage(p.previousPanelImage, nil) 65 | } 66 | 67 | return panelImage 68 | } 69 | 70 | func (p *Panel) shouldRefresh() bool { 71 | return true 72 | } 73 | 74 | func (p *Panel) renderDividingLine(panelImage *ebiten.Image) { 75 | ebitenutil.DrawRect(panelImage, float64(panelWidth)-1, 0, float64(panelWidth), float64(panelHeight), color.White) 76 | } 77 | 78 | func (p *Panel) renderTitle(panelImage *ebiten.Image) { 79 | bounds := text.BoundString(r.FontInversionz40, "protozoa") 80 | text.Draw(panelImage, "protozoa", r.FontInversionz40, titleXOffset, titleYOffset+bounds.Dy(), color.White) 81 | } 82 | 83 | func (p *Panel) renderKeyBindingText(panelImage *ebiten.Image) { 84 | message := "[Space] to Pause\n[M] to Change Mode\n[O] to Auto Select" 85 | if p.simulation.IsPaused() { 86 | message = "[Space] to Resume\n[M] to Change Mode" 87 | } 88 | 89 | bounds := text.BoundString(r.FontSourceCodePro10, message) 90 | xOffset := panelWidth - playXOffset - bounds.Dx() 91 | text.Draw(panelImage, message, r.FontSourceCodePro10, xOffset, playYOffset+bounds.Dy(), color.White) 92 | } 93 | 94 | func (p *Panel) renderStats(panelImage *ebiten.Image) { 95 | statsString := fmt.Sprintf("CYCLE: %9d\nORGANISMS: %5d\nDEAD: %10d", 96 | p.simulation.Cycle(), p.simulation.OrganismCount(), p.simulation.GetDeadCount()) 97 | text.Draw(panelImage, statsString, r.FontSourceCodePro12, statsXOffset, statsYOffset, color.White) 98 | } 99 | 100 | func (p *Panel) renderGraph(panelImage *ebiten.Image) { 101 | text.Draw(panelImage, "HISTORY", r.FontSourceCodePro12, graphXOffset, graphYOffset, color.White) 102 | graphImage := p.graph.Render() 103 | graphOptions := &ebiten.DrawImageOptions{} 104 | scaleX := float64(graphWidth) / float64(graphImage.Bounds().Dx()) 105 | scaleY := float64(graphHeight) / float64(graphImage.Bounds().Dy()) 106 | graphOptions.GeoM.Scale(scaleX, scaleY) 107 | graphOptions.GeoM.Translate(graphXOffset, graphYOffset+10) 108 | 109 | panelImage.DrawImage(graphImage, graphOptions) 110 | 111 | // draw border around graph 112 | left, top, right, bottom := float64(graphXOffset), float64(graphYOffset+10), float64(graphXOffset+graphWidth), float64(graphYOffset+graphHeight+10) 113 | ebitenutil.DrawLine(panelImage, left, top, right, top, color.White) 114 | ebitenutil.DrawLine(panelImage, right, top, right, bottom, color.White) 115 | ebitenutil.DrawLine(panelImage, left, bottom, right, bottom, color.White) 116 | ebitenutil.DrawLine(panelImage, left, top, left, bottom, color.White) 117 | } 118 | 119 | func (p *Panel) renderSelected(panelImage *ebiten.Image) { 120 | id := p.simulation.GetSelected() 121 | info := p.simulation.GetOrganismInfoByID(id) 122 | traits, found := p.simulation.GetOrganismTraitsByID(id) 123 | 124 | decisionTree := p.simulation.GetOrganismDecisionTreeByID(id) 125 | if info == nil || decisionTree == nil || found == false { 126 | return 127 | } 128 | decisionTreeString := fmt.Sprintf("DECISION TREE:\n%s", decisionTree.Print()) 129 | infoString := fmt.Sprintf("ORGANISM ID: %7d HEALTH: %[4]*.[3]*[2]f", info.ID, info.Health, 2, 5) 130 | infoString += fmt.Sprintf("\nANCESTOR ID: %7d SIZE: %5.2f", info.AncestorID, info.Size) 131 | infoString += fmt.Sprintf("\nAGE: %7d CHILDREN: %7d", info.Age, info.Children) 132 | infoString += fmt.Sprintf("\nMUTATE CHANCE: %3.0f%% SPAWN HEALTH: %[4]*.[3]*[2]f", traits.ChanceToMutateDecisionTree*100.0, traits.MinHealthToSpawn, 2, 5) 133 | infoString += fmt.Sprintf("\nPH TOLERANCE: %1.1f-%1.1f PH EFFECT: %+1.5f", traits.IdealPh-traits.PhTolerance, traits.IdealPh+traits.PhTolerance, traits.PhGrowthEffect) 134 | bounds := text.BoundString(r.FontSourceCodePro12, infoString) 135 | offsetY := selectedYOffset + bounds.Dy() + padding 136 | 137 | text.Draw(panelImage, infoString, r.FontSourceCodePro12, selectedXOffset, selectedYOffset, color.White) 138 | text.Draw(panelImage, decisionTreeString, r.FontSourceCodePro10, selectedXOffset, offsetY, color.White) 139 | } 140 | -------------------------------------------------------------------------------- /organism/traits.go: -------------------------------------------------------------------------------- 1 | package organism 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | 7 | "github.com/lucasb-eyer/go-colorful" 8 | 9 | c "github.com/Zebbeni/protozoa/config" 10 | ) 11 | 12 | const ( 13 | maxHueMutation = 5.0 14 | maxSaturationMutation = 0.05 15 | maxSaturation = 1.0 16 | minSaturation = 0.5 17 | maxLuminanceMutation = 0.05 18 | maxLuminance = 0.8 19 | minLuminance = 0.4 20 | ) 21 | 22 | // Traits contains organism-specific values that dictate how and when organisms 23 | // perform certain activities, which are passed down from parents to children. 24 | type Traits struct { 25 | OrganismColor colorful.Color 26 | // MaxSize represents the maximum size an organism can reach. 27 | MaxSize float64 28 | // SpawnHealth: The health value - and size - this organism and its 29 | // children start with, also equal to what it loses when spawning a child. 30 | SpawnHealth float64 31 | // MinHealthToSpawn: the minimum health needed in order to spawn- 32 | // must be greater than spawnHealth and less than maxSize 33 | MinHealthToSpawn float64 34 | MinCyclesBetweenSpawns int 35 | ChanceToMutateDecisionTree float64 36 | // IdealPh: the middle of the ph range the organism can tolerate without 37 | // suffering health damage 38 | IdealPh float64 39 | // PhTolerance: the distance from IdealPh the organism can handle without 40 | // suffering health effects due to ph 41 | PhTolerance float64 42 | // PhGrowthEffect: the effect organism has on the environment's ph level at its 43 | // current location, a small positive or negative number which gets 44 | // multiplied by the organism's current size 45 | PhGrowthEffect float64 46 | } 47 | 48 | func newRandomTraits() Traits { 49 | organismColor := getRandomColor() 50 | maxSize := rand.Float64() * c.MaximumMaxSize() 51 | spawnHealth := rand.Float64() * maxSize * c.MaxSpawnHealthPercent() 52 | minHealthToSpawn := spawnHealth + rand.Float64()*(maxSize-spawnHealth) 53 | minCyclesBetweenSpawns := rand.Intn(c.MaxCyclesBetweenSpawns()) 54 | chanceToMutateDecisionTree := math.Max(c.MinChanceToMutateDecisionTree(), rand.Float64()*c.MaxChanceToMutateDecisionTree()) 55 | idealPh := (c.MaxIdealPh() + c.MinIdealPh()) / 2.0 56 | phTolerance := rand.Float64() * c.MaxPhTolerance() 57 | phGrowthEffect := rand.Float64()*(c.MaxOrganismPhGrowthEffect()*2.0) - c.MaxOrganismPhGrowthEffect() 58 | return Traits{ 59 | OrganismColor: organismColor, 60 | MaxSize: maxSize, 61 | SpawnHealth: spawnHealth, 62 | MinHealthToSpawn: minHealthToSpawn, 63 | MinCyclesBetweenSpawns: minCyclesBetweenSpawns, 64 | ChanceToMutateDecisionTree: chanceToMutateDecisionTree, 65 | IdealPh: idealPh, 66 | PhTolerance: phTolerance, 67 | PhGrowthEffect: phGrowthEffect, 68 | } 69 | } 70 | 71 | func (t Traits) copyMutated() Traits { 72 | organismColor := mutateColor(t.OrganismColor) 73 | // maxSize = previous +- previous +- <5.0, bounded by MinimumMaxSize and MaximumMaxSize 74 | maxSize := mutateFloat(t.MaxSize, 5.0, c.MinimumMaxSize(), c.MaximumMaxSize()) 75 | // minCyclesBetweenSpawns = previous +- <=5, bounded by 0 and MaxCyclesBetweenSpawns 76 | minCyclesBetweenSpawns := mutateInt(t.MinCyclesBetweenSpawns, 5, 0, c.MaxCyclesBetweenSpawns()) 77 | // spawnHealth = previous +- <0.5, bounded by MinSpawnHealth and maxSize 78 | spawnHealth := mutateFloat(t.SpawnHealth, 0.5, c.MinSpawnHealth(), maxSize*c.MaxSpawnHealthPercent()) 79 | // minHealthToSpawn = previous +- <5.0, bounded by spawnHealthPercent and maxSize (both calculated above) 80 | minHealthToSpawn := mutateFloat(t.MinHealthToSpawn, 5.0, spawnHealth, maxSize) 81 | // chanceToMutateDecisionTree = previous +- <0.05, bounded by MinChanceToMutateDecisionTree and MaxChanceToMutateDecisionTree 82 | chanceToMutateDecisionTree := mutateFloat(t.ChanceToMutateDecisionTree, 0.05, c.MinChanceToMutateDecisionTree(), c.MaxChanceToMutateDecisionTree()) 83 | // phEffect = previous +- 0.001, bounded by MaxOrganismPhGrowthEffect (and -1 * MaxOrganismPhGrowthEffect) 84 | phEffect := mutateFloat(t.PhGrowthEffect, .001, c.MaxOrganismPhGrowthEffect()*-1, c.MaxOrganismPhGrowthEffect()) 85 | // ideaLPh = previous += 0.1, bounded by MinIdealPh and MaxIdealPh 86 | idealPh := mutateFloat(t.IdealPh, 0.1, c.MinIdealPh(), c.MaxIdealPh()) 87 | // phTolerance = previous +- 0.1, bounded by MinPhTolerance and MaxPhTolerance 88 | phTolerance := mutateFloat(t.PhTolerance, 0.1, c.MinPhTolerance(), c.MaxPhTolerance()) 89 | return Traits{ 90 | OrganismColor: organismColor, 91 | MaxSize: maxSize, 92 | SpawnHealth: spawnHealth, 93 | MinHealthToSpawn: minHealthToSpawn, 94 | MinCyclesBetweenSpawns: minCyclesBetweenSpawns, 95 | ChanceToMutateDecisionTree: chanceToMutateDecisionTree, 96 | IdealPh: idealPh, 97 | PhTolerance: phTolerance, 98 | PhGrowthEffect: phEffect, 99 | } 100 | } 101 | 102 | func mutateFloat(value, maxChange, min, max float64) float64 { 103 | mutated := value + maxChange - rand.Float64()*maxChange*2.0 104 | return math.Min(math.Max(mutated, min), max) 105 | } 106 | 107 | func mutateInt(value, maxChange, min, max int) int { 108 | mutated := math.Round(float64(value) + rand.Float64()*float64(maxChange)*2.0 - (float64(maxChange))) 109 | return int(math.Min(math.Max(mutated, float64(min)), float64(max))) 110 | } 111 | 112 | // MutateColor returns a slight variation on a given color 113 | func mutateColor(originalColor colorful.Color) colorful.Color { 114 | h, s, l := originalColor.HSLuv() 115 | h = mutateHue(h) 116 | s = mutateSaturation(s) 117 | l = mutateLuminance(l) 118 | return colorful.HSLuv(h, s, l) 119 | } 120 | 121 | func mutateHue(h float64) float64 { 122 | return math.Mod(h+360.0+(rand.Float64()*maxHueMutation*2.0)-maxHueMutation, 360) 123 | } 124 | 125 | func mutateSaturation(s float64) float64 { 126 | s += rand.Float64()*maxSaturationMutation*2.0 - maxSaturationMutation 127 | return math.Min(math.Max(s, minSaturation), maxSaturation) 128 | } 129 | 130 | func mutateLuminance(l float64) float64 { 131 | l += rand.Float64()*maxLuminanceMutation*2.0 - maxLuminanceMutation 132 | return math.Min(math.Max(l, minLuminance), maxLuminance) 133 | } 134 | 135 | func getRandomColor() colorful.Color { 136 | h := rand.Float64() * 360.0 137 | s := minSaturation + (rand.Float64() * (maxSaturation - minSaturation)) 138 | l := minLuminance + (rand.Float64() * (maxLuminance - minLuminance)) 139 | return colorful.HSLuv(h, s, l) 140 | } 141 | -------------------------------------------------------------------------------- /manager/environment.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | c "github.com/Zebbeni/protozoa/config" 5 | "github.com/Zebbeni/protozoa/environment" 6 | "github.com/Zebbeni/protozoa/utils" 7 | "math" 8 | "sync" 9 | ) 10 | 11 | // EnvironmentManager contains an image 12 | type EnvironmentManager struct { 13 | api environment.API 14 | 15 | currentPhMap [][]float64 16 | previousPhMap [][]float64 17 | 18 | averagePh float64 19 | 20 | mutex sync.Mutex 21 | } 22 | 23 | func NewEnvironmentManager(api environment.API) *EnvironmentManager { 24 | manager := &EnvironmentManager{ 25 | api: api, 26 | } 27 | 28 | manager.initializePhMap() 29 | 30 | return manager 31 | } 32 | 33 | func (m *EnvironmentManager) initializePhMap() { 34 | gridW, gridH := c.GridUnitsWide(), c.GridUnitsHigh() 35 | m.previousPhMap = make([][]float64, gridW) 36 | m.currentPhMap = make([][]float64, gridW) 37 | for x := 0; x < gridW; x++ { 38 | m.previousPhMap[x] = make([]float64, gridH) 39 | m.currentPhMap[x] = make([]float64, gridH) 40 | for y := 0; y < gridH; y++ { 41 | // Start all locations at neutral ph 42 | val := (c.MaxInitialPh() + c.MinInitialPh()) / 2.0 43 | m.previousPhMap[x][y] = val 44 | m.currentPhMap[x][y] = val 45 | } 46 | } 47 | } 48 | 49 | func (m *EnvironmentManager) Update() { 50 | m.updatePrevCurrentPhMaps() 51 | m.diffusePhLevels() 52 | } 53 | 54 | func (m *EnvironmentManager) GetPhMap() [][]float64 { 55 | return m.currentPhMap 56 | } 57 | 58 | func (m *EnvironmentManager) GetWalls() []utils.Point { 59 | if c.UsePools() == false { 60 | return []utils.Point{} 61 | } 62 | 63 | max := (c.GridUnitsWide() / c.PoolWidth()) * (c.GridUnitsHigh() / c.PoolHeight()) 64 | points := make([]utils.Point, 0, max) 65 | for x := 0; x < c.GridUnitsWide(); x++ { 66 | for y := 0; y < c.GridUnitsHigh(); y++ { 67 | if utils.IsWall(x, y) { 68 | points = append(points, utils.Point{X: x, Y: y}) 69 | } 70 | } 71 | } 72 | return points 73 | } 74 | 75 | // GetPhAtPoint returns the current pH level of the environment at a given point 76 | func (m *EnvironmentManager) GetPhAtPoint(point utils.Point) float64 { 77 | return m.getCurrentPh(point) 78 | } 79 | 80 | func (m *EnvironmentManager) GetAveragePh() float64 { 81 | return m.averagePh 82 | } 83 | 84 | // AddPhChangeAtPoint adds a positive or negative value to pH, bounded by the 85 | // minimum and maximum pH values provided by the config 86 | func (m *EnvironmentManager) AddPhChangeAtPoint(point utils.Point, change float64) { 87 | value := change + m.getCurrentPh(point) 88 | m.setPhAtPoint(point, value) 89 | } 90 | 91 | func (m *EnvironmentManager) setPhAtPoint(point utils.Point, val float64) { 92 | prevPh := m.getPreviousPh(point) 93 | newPh := math.Max(math.Min(val, c.MaxPh()), c.MinPh()) 94 | 95 | // only flag a worthwhile update if change is passed the threshold to update 96 | incrementToDisplay := c.PhIncrementToDisplay() 97 | if int(prevPh/incrementToDisplay) != int(newPh/incrementToDisplay) { 98 | m.addUpdatedPoint(point) 99 | } 100 | 101 | m.setCurrentPh(point, newPh) 102 | } 103 | 104 | // setCurrentPh sets the current pH level of the environment at a given point 105 | func (m *EnvironmentManager) setCurrentPh(point utils.Point, ph float64) { 106 | m.mutex.Lock() 107 | m.currentPhMap[point.X][point.Y] = ph 108 | m.mutex.Unlock() 109 | } 110 | 111 | // getCurrentPh returns the current pH level of the environment at a given point 112 | func (m *EnvironmentManager) getCurrentPh(point utils.Point) float64 { 113 | m.mutex.Lock() 114 | defer m.mutex.Unlock() 115 | return m.currentPhMap[point.X][point.Y] 116 | } 117 | 118 | // getPreviousPh returns the previous pH level of the environment at a given point 119 | func (m *EnvironmentManager) getPreviousPh(point utils.Point) float64 { 120 | m.mutex.Lock() 121 | defer m.mutex.Unlock() 122 | return m.previousPhMap[point.X][point.Y] 123 | } 124 | 125 | func (m *EnvironmentManager) addUpdatedPoint(point utils.Point) { 126 | m.api.AddPhUpdate(point) 127 | } 128 | 129 | // Between cycles, swap which phMap we're treating as the 'previous' ph values 130 | // and which 'current' ph map we will be updating 131 | func (m *EnvironmentManager) updatePrevCurrentPhMaps() { 132 | prevPhMap := m.previousPhMap 133 | m.previousPhMap = m.currentPhMap 134 | m.currentPhMap = prevPhMap 135 | } 136 | 137 | // simulate diffusion of ph across the environment by adjusting each 138 | // ph value toward its neighbors' values. 139 | // Also, while iterating, calculates average ph in environment 140 | func (m *EnvironmentManager) diffusePhLevels() { 141 | gridW, gridH := c.GridUnitsWide(), c.GridUnitsHigh() 142 | diffFactor := c.PhDiffuseFactor() 143 | 144 | adjPh := func(x, y int) (float64, bool) { 145 | return m.previousPhMap[x][y], !utils.IsWall(x, y) 146 | } 147 | 148 | // return average of all diffuse-able adjacent points 149 | avgAdjPh := func(x, y int) float64 { 150 | neighbors := 0 151 | avgPh := 0.0 152 | if ph, ok := adjPh(x, (y+1)%gridH); ok { 153 | avgPh += ph 154 | neighbors++ 155 | } 156 | if ph, ok := adjPh(x, (y+gridH-1)%gridH); ok { 157 | avgPh += ph 158 | neighbors++ 159 | } 160 | if ph, ok := adjPh((x+1)%gridW, y); ok { 161 | avgPh += ph 162 | neighbors++ 163 | } 164 | if ph, ok := adjPh((x+gridW-1)%gridW, y); ok { 165 | avgPh += ph 166 | neighbors++ 167 | } 168 | return avgPh / float64(neighbors) 169 | } 170 | 171 | // return average ph of all adjacent points (even if in walls) 172 | avgAdjPhAll := func(x, y int) float64 { 173 | avgPh := 0.0 174 | ph, _ := adjPh(x, (y+1)%gridH) 175 | avgPh += ph 176 | ph, _ = adjPh(x, (y+gridH-1)%gridH) 177 | avgPh += ph 178 | ph, _ = adjPh((x+1)%gridW, y) 179 | avgPh += ph 180 | ph, _ = adjPh((x+gridW-1)%gridW, y) 181 | avgPh += ph 182 | return avgPh / 4.0 183 | } 184 | 185 | totalPh := 0.0 186 | pointCount := float64(gridW * gridH) 187 | // set each value in the current phMap to its value in the previous phMap, plus 188 | // the average difference between itself and its N,S,E,W neighbors (times the 189 | // diffusion factor provided by the config) 190 | for x := 0; x < gridW; x++ { 191 | for y := 0; y < gridH; y++ { 192 | prevVal := m.previousPhMap[x][y] 193 | totalPh += prevVal 194 | 195 | // Just set wall ph to the average of its neighbors 196 | // (doesn't really affect anything but appearance, since we don't 197 | // diffuse this value back to the rest of the environment 198 | if utils.IsWall(x, y) { 199 | m.setPhAtPoint(utils.Point{X: x, Y: y}, avgAdjPhAll(x, y)) 200 | continue 201 | } 202 | 203 | avgAdjacentPh := avgAdjPh(x, y) 204 | change := (avgAdjacentPh - prevVal) * diffFactor 205 | m.setPhAtPoint(utils.Point{X: x, Y: y}, prevVal+change) 206 | } 207 | } 208 | 209 | m.averagePh = totalPh / pointCount 210 | } 211 | -------------------------------------------------------------------------------- /ux/graph.go: -------------------------------------------------------------------------------- 1 | package ux 2 | 3 | import ( 4 | "image" 5 | "math" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | 9 | c "github.com/Zebbeni/protozoa/config" 10 | s "github.com/Zebbeni/protozoa/simulation" 11 | ) 12 | 13 | const ( 14 | realGraphWidth = 1000.0 15 | realGraphHeight = 1000.0 16 | ) 17 | 18 | type Graph struct { 19 | simulation *s.Simulation 20 | graphImage *ebiten.Image 21 | 22 | maxTotalPopulation int 23 | } 24 | 25 | func NewGraph(sim *s.Simulation) *Graph { 26 | return &Graph{ 27 | simulation: sim, 28 | graphImage: nil, 29 | } 30 | } 31 | 32 | // TODO: Maintain a large, previously-drawn graph image and draw it to a new one that 33 | // is slightly wider when we need to re-render, so we can just draw the newest 34 | // population bar instead of re-rendering the full history. We can scale it down 35 | // to whatever dimensions we need when we return it. 36 | func (g *Graph) Render() *ebiten.Image { 37 | if g.simulation.IsPaused() { 38 | return g.graphImage 39 | } 40 | 41 | var img *ebiten.Image 42 | if g.shouldRefresh() { 43 | img = g.renderAll() 44 | } else if g.shouldAddBar() { 45 | img = g.renderNewBar() 46 | } else { 47 | return g.graphImage 48 | } 49 | 50 | g.graphImage = ebiten.NewImage(realGraphWidth, realGraphHeight) 51 | g.graphImage.DrawImage(img, nil) 52 | 53 | return img 54 | } 55 | 56 | func (g *Graph) renderAll() *ebiten.Image { 57 | // add 1 to make sure cycle 0 gives us a bar count of 1 58 | barCount := 1 + (g.simulation.Cycle() / c.PopulationUpdateInterval()) 59 | barWidth := realGraphWidth / float64(barCount) 60 | g.maxTotalPopulation = g.getMaxPopulation() 61 | img := ebiten.NewImage(realGraphWidth, realGraphHeight) 62 | 63 | for cycle := 0; cycle <= g.simulation.Cycle(); cycle += c.PopulationUpdateInterval() { 64 | barImage, graphBarPopulation := g.renderGraphBar(cycle) 65 | options := &ebiten.DrawImageOptions{} 66 | scaleX := barWidth / float64(barImage.Bounds().Dx()) 67 | scaleY := float64(graphBarPopulation) / float64(g.maxTotalPopulation) 68 | xOffset := float64(cycle/c.PopulationUpdateInterval()) * barWidth 69 | yOffset := realGraphHeight - (float64(barImage.Bounds().Dy()) * scaleY) 70 | options.GeoM.Scale(scaleX, scaleY) 71 | options.GeoM.Translate(xOffset, yOffset) 72 | img.DrawImage(barImage, options) 73 | } 74 | 75 | return img 76 | } 77 | 78 | func (g *Graph) renderNewBar() *ebiten.Image { 79 | barCount := 1 + (g.simulation.Cycle() / c.PopulationUpdateInterval()) 80 | barWidth := realGraphWidth / float64(barCount) 81 | barImage, graphBarPopulation := g.renderGraphBar(g.simulation.Cycle()) 82 | 83 | img := ebiten.NewImage(realGraphWidth, realGraphHeight) 84 | 85 | originalOptions := &ebiten.DrawImageOptions{} 86 | xScaleOriginal := (float64(barCount) - 1) / float64(barCount) 87 | yScaleOriginal := 1.0 88 | if graphBarPopulation > g.maxTotalPopulation { 89 | yScaleOriginal = float64(g.maxTotalPopulation) / float64(graphBarPopulation) 90 | g.maxTotalPopulation = graphBarPopulation 91 | } 92 | originalOptions.GeoM.Scale(xScaleOriginal, yScaleOriginal) 93 | xOffsetOriginal := 0.0 94 | yOffsetOriginal := realGraphHeight - float64(g.graphImage.Bounds().Dy())*yScaleOriginal 95 | originalOptions.GeoM.Translate(xOffsetOriginal, yOffsetOriginal) 96 | img.DrawImage(g.graphImage, originalOptions) 97 | 98 | newBarOptions := &ebiten.DrawImageOptions{} 99 | xScaleNewBar := barWidth / float64(barImage.Bounds().Dx()) 100 | yScaleNewBar := 1.0 101 | if graphBarPopulation < g.maxTotalPopulation { 102 | yScaleNewBar = float64(graphBarPopulation) / float64(g.maxTotalPopulation) 103 | } 104 | xOffsetNewBar := realGraphWidth - barWidth 105 | yOffsetNewBar := realGraphHeight - float64(barImage.Bounds().Dy())*yScaleNewBar 106 | newBarOptions.GeoM.Scale(xScaleNewBar, yScaleNewBar) 107 | newBarOptions.GeoM.Translate(xOffsetNewBar, yOffsetNewBar) 108 | 109 | img.DrawImage(barImage, newBarOptions) 110 | 111 | return img 112 | } 113 | 114 | // draw and return an image of the stacked graph bar for a single cycle 115 | // also return the number of 116 | func (g *Graph) renderGraphBar(cycle int) (*ebiten.Image, int) { 117 | barCount := 1 + (g.simulation.Cycle() / c.PopulationUpdateInterval()) 118 | realBarWidth := realGraphWidth / barCount 119 | 120 | populationMap := g.simulation.GetHistory() 121 | ancestorColorMap := g.simulation.GetAncestorColors() 122 | sortedAncestorIDs := g.simulation.GetAncestorsSorted() 123 | 124 | previousFamilyPopulations := populationMap[cycle-c.PopulationUpdateInterval()] 125 | prevTotal := getTotalPopulation(previousFamilyPopulations) 126 | newFamilyPopulations := populationMap[cycle] 127 | newTotal := getTotalPopulation(newFamilyPopulations) 128 | 129 | maxTotal := math.Max(float64(newTotal), float64(prevTotal)) 130 | heightPerPop := realGraphHeight / maxTotal 131 | 132 | barImage := ebiten.NewImage(realBarWidth, realGraphHeight) 133 | 134 | newBottom, prevBottom := float32(realGraphHeight), float32(realGraphHeight) 135 | for _, id := range sortedAncestorIDs { 136 | prevX1, prevY1, prevX2, prevY2 := float32(0), prevBottom, float32(0), prevBottom 137 | newX1, newY1, newX2, newY2 := float32(realBarWidth), newBottom, float32(realBarWidth), newBottom 138 | 139 | prevPopulation, foundPrevious := previousFamilyPopulations[id] 140 | if foundPrevious { 141 | popHeight := float32(prevPopulation) * float32(heightPerPop) 142 | prevBottom -= popHeight 143 | prevY2 = prevBottom 144 | } 145 | newPopulation, foundNew := newFamilyPopulations[id] 146 | if foundNew { 147 | popHeight := float32(newPopulation) * float32(heightPerPop) 148 | newBottom -= popHeight 149 | newY2 = newBottom 150 | } 151 | if foundPrevious == false && foundNew == false { 152 | continue 153 | } 154 | 155 | prevV1 := createVertex(prevX1, prevY1) 156 | prevV2 := createVertex(prevX2, prevY2) 157 | newV1 := createVertex(newX1, newY1) 158 | newV2 := createVertex(newX2, newY2) 159 | vertexes := make([]ebiten.Vertex, 0, 6) 160 | 161 | emptyImage := ebiten.NewImage(1, 1) 162 | emptyImage.Fill(ancestorColorMap[id]) 163 | 164 | src := emptyImage.SubImage(image.Rect(0, 0, 1, 1)).(*ebiten.Image) 165 | 166 | vertexes = append(vertexes, prevV1, prevV2, newV1, newV2) 167 | indices := []uint16{0, 1, 2, 2, 1, 3} 168 | 169 | barImage.DrawTriangles(vertexes, indices, src, nil) 170 | } 171 | 172 | return barImage, int(maxTotal) 173 | } 174 | 175 | func createVertex(x, y float32) ebiten.Vertex { 176 | return ebiten.Vertex{ 177 | DstX: x, 178 | DstY: y, 179 | SrcX: 0, 180 | SrcY: 0, 181 | ColorR: 1.0, 182 | ColorG: 1.0, 183 | ColorB: 1.0, 184 | ColorA: 1.0, 185 | } 186 | } 187 | 188 | func getTotalPopulation(populationMap map[int]int32) int { 189 | total := int32(0) 190 | for _, population := range populationMap { 191 | total += population 192 | } 193 | return int(total) 194 | } 195 | 196 | func (g *Graph) getMaxPopulation() int { 197 | maxTotal := int32(0) 198 | for cycle := 0; cycle <= g.simulation.Cycle(); cycle += c.PopulationUpdateInterval() { 199 | total := g.getPopulationByCycle(cycle) 200 | if total > maxTotal { 201 | maxTotal = total 202 | } 203 | } 204 | return int(maxTotal) 205 | } 206 | 207 | func (g *Graph) getPopulationByCycle(cycle int) int32 { 208 | populationMap := g.simulation.GetHistory() 209 | populationAtCycle, ok := populationMap[cycle] 210 | if !ok { 211 | return 0 212 | } 213 | 214 | total := int32(0) 215 | for _, familyPopulation := range populationAtCycle { 216 | total += familyPopulation 217 | } 218 | return total 219 | } 220 | 221 | func (g *Graph) shouldRefresh() bool { 222 | return g.graphImage == nil 223 | } 224 | 225 | func (g *Graph) shouldAddBar() bool { 226 | return g.simulation.Cycle()%c.PopulationUpdateInterval() == 0 227 | } 228 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 2 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk= 3 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 4 | github.com/hajimehoshi/bitmapfont/v2 v2.1.3 h1:JefUkL0M4nrdVwVq7MMZxSTh6mSxOylm+C4Anoucbb0= 5 | github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= 6 | github.com/hajimehoshi/ebiten/v2 v2.2.7 h1:OnZcSzF9wROc+7ldVAkNbdw8eoR8E/qkpOEiyk1h0H4= 7 | github.com/hajimehoshi/ebiten/v2 v2.2.7/go.mod h1:oVHP648rsA6B9pizQGjN/m2bVy0EJxAZizUxiFAESl4= 8 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 9 | github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 10 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 11 | github.com/hajimehoshi/oto/v2 v2.1.0-alpha.2/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ= 12 | github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 13 | github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4= 14 | github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4= 15 | github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= 16 | github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= 17 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 18 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 19 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 20 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 21 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 22 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 25 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 26 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 27 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 28 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 29 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 30 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 31 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 32 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 33 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 34 | golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= 35 | golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= 36 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 37 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 38 | golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5 h1:peBP2oZO/xVnGMaWMCyFEI0WENsGj71wx5K12mRELHQ= 39 | golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5/go.mod h1:c4YKU3ZylDmvbw+H/PSvm42vhdWbuxCzbonauEAP9B8= 40 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 41 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 42 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 43 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 44 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 47 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 48 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 49 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 50 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 51 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= 54 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 56 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 67 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 69 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 70 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 71 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 72 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 74 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 75 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 78 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 79 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 80 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 81 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 82 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | -------------------------------------------------------------------------------- /config/globals.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | ) 8 | 9 | var defaultFilePath = "settings/default.json" 10 | var constants *Globals 11 | 12 | // SetGlobals allows a one-time initialization of all globally-referenced constants 13 | func SetGlobals(g *Globals) { 14 | if constants != nil { 15 | return 16 | } 17 | constants = g 18 | } 19 | 20 | func GridUnitSize() int { return constants.GridUnitSize } 21 | func GridWidth() int { return constants.GridWidth } 22 | func GridHeight() int { return constants.GridHeight } 23 | func GridUnitsWide() int { return constants.GridUnitsWide } 24 | func GridUnitsHigh() int { return constants.GridUnitsHigh } 25 | func ScreenWidth() int { return constants.ScreenWidth } 26 | func ScreenHeight() int { return constants.ScreenHeight } 27 | func PopulationUpdateInterval() int { return constants.PopulationUpdateInterval } 28 | func InitialOrganisms() int { return constants.InitialOrganisms } 29 | func InitialFood() int { return constants.InitialFood } 30 | func ChanceToAddFoodItem() float64 { return constants.ChanceToAddFoodItem } 31 | func MaxFoodValue() int { return constants.MaxFoodValue } 32 | func MinFoodValue() int { return constants.MinFoodValue } 33 | func MinPh() float64 { return constants.MinPh } 34 | func MaxPh() float64 { return constants.MaxPh } 35 | func MinInitialPh() float64 { return constants.MinInitialPh } 36 | func MaxInitialPh() float64 { return constants.MaxInitialPh } 37 | func MaxCyclesBetweenSpawns() int { return constants.MaxCyclesBetweenSpawns } 38 | func MinSpawnHealth() float64 { return constants.MinSpawnHealth } 39 | func MaxSpawnHealthPercent() float64 { return constants.MaxSpawnHealthPercent } 40 | func InitialDecisionTreeMutations() int { return constants.InitialDecisionTreeMutations } 41 | func MinChanceToMutateDecisionTree() float64 { return constants.MinChanceToMutateDecisionTree } 42 | func MaxChanceToMutateDecisionTree() float64 { return constants.MaxChanceToMutateDecisionTree } 43 | func MinOrganisms() int { return constants.MinOrganisms } 44 | func MaxOrganisms() int { return constants.MaxOrganisms } 45 | func GrowthFactor() float64 { return constants.GrowthFactor } 46 | func MaximumMaxSize() float64 { return constants.MaximumMaxSize } 47 | func MinimumMaxSize() float64 { return constants.MinimumMaxSize } 48 | func MinIdealPh() float64 { return constants.MinIdealPh } 49 | func MaxIdealPh() float64 { return constants.MaxIdealPh } 50 | func MinPhTolerance() float64 { return constants.MinPhTolerance } 51 | func MaxPhTolerance() float64 { return constants.MaxPhTolerance } 52 | func MaxOrganismPhGrowthEffect() float64 { return constants.MaxOrganismPhGrowthEffect } 53 | func PhIncrementToDisplay() float64 { return constants.PhIncrementToDisplay } 54 | func PhDiffuseFactor() float64 { return constants.PhDiffuseFactor } 55 | func UsePools() bool { return constants.UsePools } 56 | func PoolWidth() int { return constants.PoolWidth } 57 | func PoolHeight() int { return constants.PoolHeight } 58 | func HealthChangeFromChemosynthesis() float64 { return constants.HealthChangeFromChemosynthesis } 59 | func HealthChangeFromTurning() float64 { return constants.HealthChangeFromTurning } 60 | func HealthChangeFromMoving() float64 { return constants.HealthChangeFromMoving } 61 | func HealthChangeFromEatingAttempt() float64 { return constants.HealthChangeFromEatingAttempt } 62 | func HealthChangeFromAttacking() float64 { return constants.HealthChangeFromAttacking } 63 | func HealthChangeInflictedByAttack() float64 { return constants.HealthChangeInflictedByAttack } 64 | func HealthChangeFromFeeding() float64 { return constants.HealthChangeFromFeeding } 65 | func HealthChangePerDecisionTreeNode() float64 { return constants.HealthChangePerDecisionTreeNode } 66 | func HealthChangePerUnhealthyPh() float64 { return constants.HealthChangePerCycleUnhealthyPh } 67 | func MaxDecisionTreeSize() int { return constants.MaxDecisionTreeSize } 68 | 69 | type Globals struct { 70 | // Drawing parameters 71 | GridUnitSize int `json:"grid_unit_size"` 72 | GridWidth int `json:"grid_width"` 73 | GridHeight int `json:"grid_height"` 74 | GridUnitsWide int `json:"grid_units_wide"` 75 | GridUnitsHigh int `json:"grid_units_high"` 76 | ScreenWidth int `json:"screen_width"` 77 | ScreenHeight int `json:"screen_height"` 78 | 79 | // Statistics parameters 80 | PopulationUpdateInterval int `json:"population_update_interval"` 81 | 82 | // Environment parameters 83 | InitialOrganisms int `json:"initial_organisms"` 84 | InitialFood int `json:"initial_food"` 85 | ChanceToAddFoodItem float64 `json:"chance_to_add_food_item"` 86 | MaxFoodValue int `json:"max_food_value"` 87 | MinFoodValue int `json:"min_food_value"` 88 | MinPh float64 `json:"min_ph"` 89 | MaxPh float64 `json:"max_ph"` 90 | MinInitialPh float64 `json:"min_initial_ph"` 91 | MaxInitialPh float64 `json:"max_initial_ph"` 92 | 93 | // Organism parameters 94 | MaxCyclesBetweenSpawns int `json:"max_cycles_between_spawns"` 95 | MinSpawnHealth float64 `json:"min_spawn_health"` 96 | MaxSpawnHealthPercent float64 `json:"max_spawn_health_percent"` 97 | MinOrganisms int `json:"min_organisms"` 98 | MaxOrganisms int `json:"max_organisms"` 99 | GrowthFactor float64 `json:"growth_factor"` 100 | MaximumMaxSize float64 `json:"maximum_max_size"` 101 | MinimumMaxSize float64 `json:"minimum_max_size"` 102 | InitialDecisionTreeMutations int `json:"initial_organism_decision_tree_mutations"` 103 | MinChanceToMutateDecisionTree float64 `json:"min_chance_to_mutate_decision_tree"` 104 | MaxChanceToMutateDecisionTree float64 `json:"max_chance_to_mutate_decision_tree"` 105 | MaxDecisionTreeSize int `json:"max_decision_tree_size"` 106 | MinIdealPh float64 `json:"min_ideal_ph"` 107 | MaxIdealPh float64 `json:"max_ideal_ph"` 108 | MinPhTolerance float64 `json:"min_ph_tolerance"` 109 | MaxPhTolerance float64 `json:"max_ph_tolerance"` 110 | MaxOrganismPhGrowthEffect float64 `json:"max_organism_ph_growth_effect"` 111 | MinChangeToPh float64 `json:"min_change_to_ph"` 112 | MaxChangeToPh float64 `json:"max_change_to_ph"` 113 | PhIncrementToDisplay float64 `json:"ph_increment_to_display"` 114 | PhDiffuseFactor float64 `json:"ph_diffuse_factor"` 115 | UsePools bool `json:"use_pools"` 116 | PoolWidth int `json:"pool_width"` 117 | PoolHeight int `json:"pool_height"` 118 | 119 | // Health parameters (percent of organism size) 120 | HealthChangeFromChemosynthesis float64 `json:"health_change_from_chemosynthesis"` 121 | HealthChangeFromTurning float64 `json:"health_change_from_turning"` 122 | HealthChangeFromMoving float64 `json:"health_change_from_moving"` 123 | HealthChangeFromEatingAttempt float64 `json:"health_change_from_eating_attempt"` 124 | HealthChangeFromAttacking float64 `json:"health_change_from_attacking"` 125 | HealthChangeInflictedByAttack float64 `json:"health_change_inflicted_by_attack"` 126 | HealthChangeFromFeeding float64 `json:"health_change_from_feeding"` 127 | HealthChangePerDecisionTreeNode float64 `json:"health_change_per_decision_tree_node"` 128 | HealthChangePerCycleUnhealthyPh float64 `json:"health_change_per_unhealthy_ph"` 129 | } 130 | 131 | func LoadFile(filePath string) io.Reader { 132 | file, err := os.Open(filePath) 133 | if err != nil { 134 | panic("failed to read config file") 135 | } 136 | return file 137 | } 138 | 139 | func GetDefaultGlobals() Globals { 140 | defaultFile := LoadFile(defaultFilePath) 141 | g := applyGlobalsFromJson(defaultFile, Globals{}) 142 | return *g 143 | } 144 | 145 | func LoadGlobals(file io.Reader) *Globals { 146 | defaults := GetDefaultGlobals() 147 | g := applyGlobalsFromJson(file, defaults) 148 | return g 149 | } 150 | 151 | func applyGlobalsFromJson(file io.Reader, globals Globals) *Globals { 152 | g := globals 153 | decoder := json.NewDecoder(file) 154 | err := decoder.Decode(&g) 155 | if err != nil { 156 | panic("failed to read globals from file") 157 | } 158 | return &g 159 | } 160 | 161 | func DumpGlobals(g *Globals, file io.Writer) { 162 | data, err := json.MarshalIndent(g, "", " ") 163 | if err != nil { 164 | panic("failed to convert globals to json") 165 | } 166 | 167 | _, err = file.Write(data) 168 | if err != nil { 169 | panic("failed to write globals to file") 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protozoa 2 | A simulation of organisms navigating their environment according to inherited traits and decision trees. 3 | Rendered with [ebitengine](https://github.com/hajimehoshi/ebiten) 4 | 5 | ![Screen Shot 2022-07-30 at 12 33 53 AM](https://user-images.githubusercontent.com/3377325/181996580-b8a51e8c-0310-43ba-bcb5-3f189a59486b.png) 6 | 7 | ## Simulation Rules 8 | Protozoa randomly generates a number of organisms and food items on a 2D grid. Per render cycle, each organism chooses a simple action (eat, move, turn, attack etc.) based on a randomly-generated decision tree with which it was initialized. Organisms that survive long enough can spawn offspring with very slight mutations, thus propagating successful traits and behaviors. 9 | 10 | ### Environment 11 | The environment consists of a 2D wraparound grid. Each location contains a ph value (0-10). These ph values play a large role in organism health, and are likewise affected by certain organism actions (ie. growth). 12 | 13 | Each cycle, ph values diffuse between neighboring grid locations at a regular rate, such that the whole environment will gradually approach a single ph value in the absence of organism activity. 14 | 15 | Low ph (acidic) locations appear green, high ph (alkaline) locations are pink, and neutral locations (~5.0 ph) are black. 16 | 17 | 18 | 19 | Additionally, the environment can be separated by walls into 'pools' with small openings allowing diffusion and movement in between. This is meant to allow different families of organisms to develop in isolation longer than would otherwise be possible. (The existence and size of these pools can be set in the configuration json files in `settings/`) 20 | 21 | ![Screen Shot 2022-07-30 at 1 37 57 AM](https://user-images.githubusercontent.com/3377325/181996681-40dbc369-082a-44fb-ae3a-40e33e60227a.png) 22 | 23 | ### Food 24 | 25 | 'Food' items are generated when organisms die. Each food item is represented by a dark gray square and contains a value between 0 and 100, representing how much the food item contains. When an organism sees a food item directly ahead, it can choose to 'eat' it, subtracting some value from the food and adding it to its own health. If a food item's value is reduced to 0, it disappears from the grid. Conversely, when an organism's health is reduced to 0 it 'dies' and is immediately replaced with a food item, whose value is set equal to the organism's size at death. 26 | 27 | Apart from feeding organisms, food items also prevent movement. Organisms and food items cannot occupy the same location, and an organism facing a food item directly ahead cannot move through it. 28 | 29 | ![Food Items](https://user-images.githubusercontent.com/3377325/165467819-fb51b843-5fe3-422c-adf3-21212d65b1e3.png) 30 | 31 | ### Organisms 32 | 33 | Organisms are represented by colored squares of different sizes, and they perform actions in their environment according to a set of genetic traits and a single decision tree. 'Health' and 'energy' are the same thing for organisms, and an organism's actions (moving, eating, etc.) may reduce its own health by some small amount to represent the energy exertion needed to do them. Further, an organism unable to tolerate the ph of its location will also have its health reduced until conditions improve. 34 | 35 | An organism's health is limited by its current size, so an organism of size 50 will have a max health of 50. When an organism gains more health than its size allows, it 'grows' in size by some fraction of the excess health gain. 36 | 37 | #### Traits 38 | Initial organisms are generated with random values for several 'genetic' traits that define its size limitations, its ph tolerance, the time it waits betweeen spawning, etc. When spawning a new organism, the traits of the parent are adjusted by small random amounts and passed down to the new child. 39 | * **Color -** _generated from random hue, saturation, and brightness_ 40 | * **MaxSize -** _the maximum size an organism can grow_ 41 | * **SpawnHealth -** _the initial health given to a spawned child, which is also subtracted from the parent's health_ 42 | * **MinHealthToSpawn -** _the minimum health required by the parent to spawn a new child (never less than SpawnHealth)_ 43 | * **MinCyclesBetweenSpawns -** _the minimum number of cycles that must pass before the organism can produce another child_ 44 | * **ChanceToMutateDecisionTree -** _The chance of the organism passing a mutated version of its decision tree onto each spawned child_ 45 | * **IdealPh -** _The middle of the organism's ph tolerance range_ 46 | * **PhTolerance -** _The absolute ph distance the organism can go from its ideal ph without adverse effects. (eg. An ideal ph of 3 and ph tolerance of 1 provide a tolerance zone of 2-4 ph)_ 47 | * **PhEffect -** _the positive or negative factor the organism's growth has on the ph level of its location)_ 48 | 49 | #### Decision Trees 50 | Each organism's behavior is governed by a decision tree composed of various conditions and actions. Organisms generated at simulation start are given randomly-selected trees built from these decision nodes, while spawned children inherit an identical or similar variation of their parents' decision tree. and chosen from the following: 51 | ##### Conditions 52 | * **CanMoveAhead -** _checks if the organism can move forward (false if a food item or another organism directly ahead)_ 53 | * **IsRandomFiftyPercent -** _returns true if a randomly generated float is less than .5_ 54 | * **IsFoodAhead -** _true if a food item directly ahead_ 55 | * **IsFoodLeft -** _true if a food item lies 90 degrees to the left_ 56 | * **IsFoodRight -** _true if a food item lies 90 degrees to the right_ 57 | * **IsOrganismAhead -** _true if an organism directly ahead_ 58 | * **IsOrganismLeft -** _true if an organism lies 90 degrees to the left_ 59 | * **IsOrganismRight -** _true if an organism lies 90 degrees to the right_ 60 | * **IsBiggerOrganismAhead -** _true if an organism of greater size directly ahead_ 61 | * **IsRelatedOrganismAhead -** _true if an organism with a shared ancestor directly ahead_ 62 | * **IsRelatedOrganismLeft -** _true if an organism with a shared ancestor lies 90 degrees to the left_ 63 | * **IsRelatedOrganismRight -** _true if an organism with a shared ancestor lies 90 degrees to the right_ 64 | * **IfHealthAboveFiftyPercent -** _true if organism's health values more than half its current size_ 65 | * **IsHealthyPhHere -** _true if the ph level at current location is within the organism's tolerance - having no harmful health effects and allowing for chemosynthesis_ 66 | * **IsHealthierPhAhead -** _true if the ph level directly ahead is closer to the organism's ideal ph than the ph at its current location tolerance_ 67 | ##### Actions 68 | * **Chemosynthesis -** _generates a small amount of health, if performed at a location with healthy ph_ 69 | * **Eat -** _consumes a small amount of health to consume any food that lies directly ahead_ 70 | * **Move -** _consumes a small amount of health to move forward, if no food or organism directly ahead_ 71 | * **TurnLeft --** _consumes a small amount of health to turn 90 degrees left_ 72 | * **TurnRight -** _consumes a small amount of health to turn 90 degrees right_ 73 | * **Attack -** _consumes a large amount of health to reduce the health of any organism directly ahead_ 74 | * **Feed -** _transfers a small amount of health to any organism directly ahead_ 75 | 76 | ##### Decision Tree Health Effects 77 | Because decision trees are randomly generated and mutated, many trees will have areas of redundancy and illogic, containing branches that have no possibility of ever being reached. As a way to reward logical algorithms, Organisms lose a very small amount of health each cycle for every node in their decision tree, as a way to simulate the energy needed to process complicated decision-making. Thus, over time, subsequent mutations to decision trees should allow more efficient organisms to outpace those with similar behaviors but less efficient algorithms. 78 | 79 | #### Display 80 | Clicking on an organism in the simulation grid will display its traits and decision tree in the left-hand panel, as shown: 81 | 82 | ![Screen Shot 2022-04-26 at 9 14 18 PM](https://user-images.githubusercontent.com/3377325/165596847-a73b1ae0-5ad4-4bf0-96c2-fa8479a3fb48.png) ![Decision Tree](https://user-images.githubusercontent.com/3377325/165603440-53925db2-e02d-4dc7-944b-1b73506a5197.jpg) 83 | 84 | As printed, each conditional statement (eg. "If Can Move Ahead") is followed by a line that splits into two branches. The first, top-most branch is the logic the organism will follow if the checked condition returns true. The second, bottom branch will evaluate if the condition returns false. All decision tree nodes evaluated in the previous cycle are followed by "◀◀". Thus, the example decision tree shows - in the previous cycle - the selected organism checked 'If Can Move Ahead' (true), checked 'If Food Right' (false), and so it chose the 'Move Ahead' action. 85 | 86 | # Setup 87 | ``` 88 | go get 89 | go run main.go 90 | ``` 91 | ## Run Options 92 | ```-config``` Use overriden simulation constants. Ex: 93 | ``` 94 | go run main.go -config=settings/small.json 95 | go run main.go -config=settings/big.json 96 | ``` 97 | ```-seed``` Set the random seed used by the simulation. Ex: 98 | ``` 99 | go run main.go -seed=2 100 | ``` 101 | ```-debug``` Display memory usage and FPS 102 | ``` 103 | go run main.go -debug=true 104 | ``` 105 | 106 | # Config 107 | You can create your own .json config files to override simulation constants at runtime. 108 | To print the default settings as json (you can paste and edit this in a new configuration .json file) 109 | ``` 110 | go run main.go -dump-config 111 | ``` 112 | 113 | # Run Headless 114 | - Single trial: 115 | ``` 116 | go run main.go -headless 117 | ``` 118 | - Multiple trials: 119 | ``` 120 | go run main.go -headless -trials=10 121 | ``` 122 | 123 | # Test 124 | ``` 125 | go test test/utils_test.go 126 | ``` 127 | -------------------------------------------------------------------------------- /simulation/simulation.go: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import ( 4 | "fmt" 5 | d "github.com/Zebbeni/protozoa/decision" 6 | "image/color" 7 | "sort" 8 | "time" 9 | 10 | "github.com/Zebbeni/protozoa/config" 11 | "github.com/Zebbeni/protozoa/food" 12 | "github.com/Zebbeni/protozoa/manager" 13 | "github.com/Zebbeni/protozoa/organism" 14 | "github.com/Zebbeni/protozoa/utils" 15 | ) 16 | 17 | // Simulation contains a list of forces, particles, and drawing settings 18 | type Simulation struct { 19 | options *config.Options 20 | 21 | cycle int 22 | isPaused bool 23 | 24 | selectedID int 25 | 26 | organismManager *manager.OrganismManager 27 | foodManager *manager.FoodManager 28 | environmentManager *manager.EnvironmentManager 29 | updateManager *manager.UpdateManager 30 | 31 | // debug statistics 32 | UpdateTime, EnvironmentUpdateTime, FoodUpdateTime, OrganismUpdateTime time.Duration 33 | OrganismUpdateLoopTime, OrganismResolveLoopTime time.Duration 34 | } 35 | 36 | // NewSimulation returns a simulation with generated world and organisms 37 | // cycle increments at the beginning of Update() so start at -1 to ensure 38 | // first actions are attributed to cycle 0 39 | func NewSimulation(options *config.Options) *Simulation { 40 | sim := &Simulation{ 41 | options: options, 42 | cycle: -1, 43 | isPaused: false, 44 | } 45 | sim.updateManager = manager.NewUpdateManager() 46 | sim.environmentManager = manager.NewEnvironmentManager(sim) 47 | sim.foodManager = manager.NewFoodManager(sim) 48 | sim.organismManager = manager.NewOrganismManager(sim) 49 | 50 | return sim 51 | } 52 | 53 | // Update calls Update functions for controllers in simulation 54 | func (s *Simulation) Update() { 55 | if s.isPaused { 56 | return 57 | } 58 | 59 | s.cycle++ 60 | start := time.Now() 61 | 62 | s.updateEnvironment() 63 | s.updateFood() 64 | s.updateOrganisms() 65 | 66 | s.UpdateTime = time.Since(start) 67 | } 68 | 69 | func (s *Simulation) updateEnvironment() { 70 | start := time.Now() 71 | s.environmentManager.Update() 72 | s.EnvironmentUpdateTime = time.Since(start) 73 | } 74 | 75 | func (s *Simulation) updateFood() { 76 | start := time.Now() 77 | s.foodManager.Update() 78 | s.FoodUpdateTime = time.Since(start) 79 | } 80 | 81 | func (s *Simulation) updateOrganisms() { 82 | start := time.Now() 83 | s.organismManager.Update() 84 | s.OrganismUpdateTime = time.Since(start) 85 | s.OrganismUpdateLoopTime = s.organismManager.UpdateDuration 86 | s.OrganismResolveLoopTime = s.organismManager.ResolveDuration 87 | } 88 | 89 | // IsDone returns true if end condition met 90 | func (s *Simulation) IsDone() bool { 91 | if s.GetNumOrganisms() == 0 { 92 | fmt.Printf("\nSimulation ended on cycle %d with %d organisms alive.", s.cycle, config.MaxOrganisms()) 93 | return true 94 | } 95 | return false 96 | } 97 | 98 | // IsDebug returns true if debug flag set on run 99 | func (s *Simulation) IsDebug() bool { 100 | return s.options.IsDebugging 101 | } 102 | 103 | // ToggleDebug returns true if debug flag set on run 104 | func (s *Simulation) ToggleDebug() { 105 | s.options.IsDebugging = s.options.IsDebugging == false 106 | } 107 | 108 | // Cycle returns the current simulation cycle number 109 | func (s *Simulation) Cycle() int { 110 | return s.cycle 111 | } 112 | 113 | // IsPaused returns whether the simulation is currently stopped 114 | func (s *Simulation) IsPaused() bool { 115 | return s.isPaused 116 | } 117 | 118 | // Pause sets isPaused to the given boolean value 119 | func (s *Simulation) Pause(pause bool) { 120 | s.isPaused = pause 121 | } 122 | 123 | // AddOrganismUpdate registers that the Organism at a point has changed in a noteworthy way 124 | func (s *Simulation) AddOrganismUpdate(point utils.Point) { 125 | s.updateManager.AddOrganismUpdate(point) 126 | } 127 | 128 | // AddPhUpdate registers that a point's ph was changed by a noteworthy amount 129 | func (s *Simulation) AddPhUpdate(point utils.Point) { 130 | s.updateManager.AddPhUpdate(point) 131 | } 132 | 133 | // AddFoodUpdate registers that a point's food value was changed by a noteworthy amount 134 | func (s *Simulation) AddFoodUpdate(point utils.Point) { 135 | s.updateManager.AddFoodUpdate(point) 136 | } 137 | 138 | // GetUpdatedFoodPoints returns a map of all points recently updated by the 139 | // foodManager 140 | func (s *Simulation) GetUpdatedFoodPoints() map[string]utils.Point { 141 | return s.updateManager.GetUpdatedFoodPoints() 142 | } 143 | 144 | // GetUpdatedOrganismPoints returns a map of all points recently updated by the 145 | // organismManager 146 | func (s *Simulation) GetUpdatedOrganismPoints() map[string]utils.Point { 147 | return s.updateManager.GetUpdatedOrganismPoints() 148 | } 149 | 150 | // GetUpdatedPhPoints returns a map of all points recently updated by the 151 | // environmentManager 152 | func (s *Simulation) GetUpdatedPhPoints() map[string]utils.Point { 153 | return s.updateManager.GetUpdatedPhPoints() 154 | } 155 | 156 | // ClearUpdatedPoints clears all updated points for all content managers 157 | func (s *Simulation) ClearUpdatedPoints() { 158 | s.updateManager.ClearMaps() 159 | } 160 | 161 | // GetAllOrganismInfo returns a map of Info on all living organisms 162 | func (s *Simulation) GetAllOrganismInfo() map[int]*organism.Info { 163 | return s.organismManager.GetAllOrganismInfo() 164 | } 165 | 166 | // GetOrganismInfoAtPoint returns the Organism at a given point (nil if none found) 167 | func (s *Simulation) GetOrganismInfoAtPoint(point utils.Point) *organism.Info { 168 | return s.organismManager.GetOrganismInfoAtPoint(point) 169 | } 170 | 171 | // GetOrganismInfoByID returns the Organism Info for a given ID (nil if none) 172 | func (s *Simulation) GetOrganismInfoByID(id int) *organism.Info { 173 | return s.organismManager.GetOrganismInfoByID(id) 174 | } 175 | 176 | // GetOrganismTraitsByID returns the Organism Traits for a given ID 177 | func (s *Simulation) GetOrganismTraitsByID(id int) (organism.Traits, bool) { 178 | return s.organismManager.GetOrganismTraitsByID(id) 179 | } 180 | 181 | // GetOldestId returns the id of the oldest living organism 182 | func (s *Simulation) GetOldestId() int { 183 | return s.organismManager.GetOldestId() 184 | } 185 | 186 | // GetMostChildrenId returns the id of the most traveled organism 187 | func (s *Simulation) GetMostChildrenId() int { 188 | return s.organismManager.GetMostChildrenId() 189 | } 190 | 191 | // GetMostTraveledId returns the id of the most traveled organism 192 | func (s *Simulation) GetMostTraveledId() int { 193 | return s.organismManager.GetMostTraveledId() 194 | } 195 | 196 | // GetOrganismDecisionTreeByID returns a copy of the currently-used decision tree of the 197 | // given organism (nil if no organism found) 198 | func (s *Simulation) GetOrganismDecisionTreeByID(id int) *d.Tree { 199 | return s.organismManager.GetOrganismDecisionTreeByID(id) 200 | } 201 | 202 | // GetHistory returns the full population history of all original ancestors as a 203 | // map of cycles to maps of ancestorIDs to the living descendants at that time 204 | func (s *Simulation) GetHistory() map[int]map[int]int32 { 205 | return s.organismManager.GetHistory() 206 | } 207 | 208 | // GetAncestorColors returns a map of all ancestors with at least one descendant 209 | // and the ancestor's color 210 | func (s *Simulation) GetAncestorColors() map[int]color.Color { 211 | return s.organismManager.GetAncestorColors() 212 | } 213 | 214 | // GetAncestorsSorted returns a list of all original ancestor IDs in order 215 | func (s *Simulation) GetAncestorsSorted() []int { 216 | ancestors := s.organismManager.GetAncestors() 217 | sort.Ints(ancestors) 218 | return ancestors 219 | } 220 | 221 | // GetNumOrganisms returns the total number of all living organisms in the simulation. 222 | func (s *Simulation) GetNumOrganisms() int { 223 | return s.organismManager.OrganismCount() 224 | } 225 | 226 | // GetDeadCount returns the total number of organisms that have died in the simulation. 227 | func (s *Simulation) GetDeadCount() int { 228 | return s.organismManager.DeadCount() 229 | } 230 | 231 | // GetFoodItems returns a map of all food items in the grid 232 | func (s *Simulation) GetFoodItems() map[string]*food.Item { 233 | return s.foodManager.GetFoodItems() 234 | } 235 | 236 | // CheckOrganismAtPoint returns the result of running a check against any 237 | // Organism object found at a given Point. 238 | func (s *Simulation) CheckOrganismAtPoint(point utils.Point, checkFunc organism.OrgCheck) bool { 239 | return s.organismManager.CheckOrganismAtPoint(point, checkFunc) 240 | } 241 | 242 | // OrganismCount returns the current number of Organisms alive in the simulation 243 | func (s *Simulation) OrganismCount() int { 244 | return s.organismManager.OrganismCount() 245 | } 246 | 247 | func (s *Simulation) AveragePh() float64 { 248 | return s.environmentManager.GetAveragePh() 249 | } 250 | 251 | // GetFoodAtPoint returns the value of any food at a given point and whether 252 | // a food item actually exists there. 253 | func (s *Simulation) GetFoodAtPoint(point utils.Point) (*food.Item, bool) { 254 | return s.foodManager.GetFoodAtPoint(point) 255 | } 256 | 257 | // CheckFoodAtPoint returns the result of running a check against any food Item 258 | // object found at a given Point. 259 | func (s *Simulation) CheckFoodAtPoint(point utils.Point, checkFunc organism.FoodCheck) bool { 260 | item, found := s.foodManager.GetFoodAtPoint(point) 261 | return checkFunc(item, found) 262 | } 263 | 264 | // AddFoodAtPoint attempts to add a food value to a given point and returns the actual 265 | // amount of food added. 266 | func (s *Simulation) AddFoodAtPoint(point utils.Point, value int) { 267 | s.foodManager.AddFoodAtPoint(point, value) 268 | } 269 | 270 | // RemoveFoodAtPoint attempts to add a food value to a given point and returns the actual 271 | // amount of food added. 272 | func (s *Simulation) RemoveFoodAtPoint(point utils.Point, value int) { 273 | s.foodManager.RemoveFoodAtPoint(point, value) 274 | } 275 | 276 | // Select sets the currently selected organism ID. -1 if none selected 277 | func (s *Simulation) Select(id int) { 278 | s.selectedID = id 279 | } 280 | 281 | // GetSelected returns the currently selected organism ID. -1 if none selected 282 | func (s *Simulation) GetSelected() int { 283 | return s.selectedID 284 | } 285 | 286 | // GetPhMap returns the full 2D map of all pH values in the environment 287 | func (s *Simulation) GetPhMap() [][]float64 { 288 | return s.environmentManager.GetPhMap() 289 | } 290 | 291 | // GetWalls returns all points in the environment that contain a wall 292 | func (s *Simulation) GetWalls() []utils.Point { 293 | return s.environmentManager.GetWalls() 294 | } 295 | 296 | // GetPhAtPoint returns the current Ph of the environment at a given location 297 | func (s *Simulation) GetPhAtPoint(point utils.Point) float64 { 298 | return s.environmentManager.GetPhAtPoint(point) 299 | } 300 | 301 | // AddPhChangeAtPoint adds a given value to the environment's Ph at a given location 302 | func (s *Simulation) AddPhChangeAtPoint(point utils.Point, change float64) { 303 | s.environmentManager.AddPhChangeAtPoint(point, change) 304 | } 305 | -------------------------------------------------------------------------------- /organism/organism.go: -------------------------------------------------------------------------------- 1 | package organism 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | "math/rand" 7 | "sync" 8 | 9 | c "github.com/Zebbeni/protozoa/config" 10 | d "github.com/Zebbeni/protozoa/decision" 11 | "github.com/Zebbeni/protozoa/food" 12 | "github.com/Zebbeni/protozoa/utils" 13 | ) 14 | 15 | type Organism struct { 16 | ID int 17 | Age int 18 | Health float64 19 | Size float64 20 | Children int 21 | TraveledDist int 22 | CyclesSinceLastSpawn int 23 | Location utils.Point 24 | Direction utils.Point 25 | OriginalAncestorID int 26 | 27 | traits Traits 28 | 29 | decisionTree *d.Tree 30 | action d.Action 31 | 32 | lookupAPI LookupAPI 33 | 34 | mutex sync.Mutex 35 | } 36 | 37 | // NewRandom initializes organism at with random grid location and direction 38 | func NewRandom(id int, point utils.Point, api LookupAPI) *Organism { 39 | traits := newRandomTraits() 40 | decisionTree := d.TreeFromAction(d.GetRandomAction()) 41 | for mutations := 0; mutations < c.InitialDecisionTreeMutations(); mutations++ { 42 | decisionTree = d.MutateTree(decisionTree) 43 | } 44 | organism := Organism{ 45 | ID: id, 46 | Age: 0, 47 | Health: traits.SpawnHealth, 48 | Size: traits.SpawnHealth, 49 | Children: 0, 50 | CyclesSinceLastSpawn: 0, 51 | Location: point, 52 | Direction: utils.GetRandomDirection(), 53 | OriginalAncestorID: id, 54 | 55 | traits: traits, 56 | decisionTree: decisionTree, 57 | action: d.ActChemosynthesis, 58 | 59 | lookupAPI: api, 60 | } 61 | return &organism 62 | } 63 | 64 | // NewChild initializes and returns a new organism with a copied TreeLibrary from its parent 65 | func (o *Organism) NewChild(id int, point utils.Point, api LookupAPI) *Organism { 66 | traits := o.traits.copyMutated() 67 | inheritedTree := o.GetDecisionTreeCopy() 68 | if rand.Float64() < o.ChanceToMutateDecisionTree() { 69 | inheritedTree = d.MutateTree(inheritedTree) 70 | } 71 | organism := Organism{ 72 | ID: id, 73 | Age: 0, 74 | Health: o.InitialHealth(), 75 | Size: o.InitialHealth(), 76 | Children: 0, 77 | CyclesSinceLastSpawn: 0, 78 | Location: point, 79 | Direction: utils.GetRandomDirection(), 80 | OriginalAncestorID: o.OriginalAncestorID, 81 | 82 | traits: traits, 83 | decisionTree: inheritedTree, 84 | action: d.ActChemosynthesis, 85 | 86 | lookupAPI: api, 87 | } 88 | return &organism 89 | } 90 | 91 | func (o *Organism) Info() *Info { 92 | return &Info{ 93 | ID: o.ID, 94 | Health: o.Health, 95 | Location: o.Location, 96 | Size: o.Size, 97 | Action: o.action, 98 | AncestorID: o.OriginalAncestorID, 99 | Color: o.traits.OrganismColor, 100 | Age: o.Age, 101 | Children: o.Children, 102 | PhEffect: o.traits.PhGrowthEffect, 103 | } 104 | } 105 | 106 | // UpdateStats runs on each cycle and updates Age, CyclesSinceLastSpawn, etc. 107 | // Also calculates the change in health since the last cycle and applies this 108 | // to the success metrics of the last-used decision tree. 109 | func (o *Organism) UpdateStats() { 110 | o.Age++ 111 | o.CyclesSinceLastSpawn++ 112 | o.decisionTree.ResetUsedLastCycle() 113 | } 114 | 115 | // UpdateAction runs on each cycle, occasionally changing the current decision 116 | // tree before running it to determine its next action 117 | func (o *Organism) UpdateAction() { 118 | if o.shouldSpawn() { 119 | o.CyclesSinceLastSpawn = 0 120 | o.action = d.ActSpawn 121 | return 122 | } 123 | o.action = o.chooseAction(o.decisionTree.Node) 124 | } 125 | 126 | func (o *Organism) shouldSpawn() bool { 127 | if o.CyclesSinceLastSpawn < o.MinCyclesBetweenSpawns() { 128 | return false 129 | } 130 | if o.Health < o.MinHealthToSpawn() { 131 | return false 132 | } 133 | if o.lookupAPI.OrganismCount() >= c.MaxOrganisms() { 134 | return false 135 | } 136 | return true 137 | } 138 | 139 | // chooseAction walks through nodes of an organism's decision tree, eventually 140 | // returning the chosen action 141 | // 142 | // As chooseAction walks through nodes, it also sets UsedLastCycle=true, allowing 143 | // the organism to attribute success or failure to the previously-chosen path 144 | func (o *Organism) chooseAction(node *d.Node) d.Action { 145 | node.UsedLastCycle = true 146 | if node.IsAction() { 147 | return node.NodeType.(d.Action) 148 | } 149 | if o.isConditionTrue(node.NodeType) { 150 | return o.chooseAction(node.YesNode) 151 | } 152 | return o.chooseAction(node.NoNode) 153 | } 154 | 155 | func (o *Organism) isConditionTrue(cond interface{}) bool { 156 | switch cond { 157 | case d.CanMove: 158 | return o.canMove() 159 | case d.IsFoodAhead: 160 | return o.isFoodAhead() 161 | case d.IsFoodLeft: 162 | return o.isFoodLeft() 163 | case d.IsFoodRight: 164 | return o.isFoodRight() 165 | case d.IsOrganismAhead: 166 | return o.isOrganismAhead() 167 | case d.IsBiggerOrganismAhead: 168 | return o.isBiggerOrganismAhead() 169 | case d.IsRelatedOrganismAhead: 170 | return o.isRelatedOrganismAhead() 171 | case d.IsOrganismLeft: 172 | return o.isOrganismLeft() 173 | case d.IsRelatedOrganismLeft: 174 | return o.isRelatedOrganismLeft() 175 | case d.IsOrganismRight: 176 | return o.isOrganismRight() 177 | case d.IsRelatedOrganismRight: 178 | return o.isRelatedOrganismRight() 179 | //case d.IsRandomFiftyPercent: 180 | // return rand.Float32() < 0.5 181 | case d.IsHealthAboveFiftyPercent: 182 | return o.Health > o.Size*0.5 183 | case d.IsHealthyPhHere: 184 | return o.isHealthyPhHere() 185 | case d.IsHealthierPhAhead: 186 | return o.isHealthierPhAhead() 187 | } 188 | return false 189 | } 190 | 191 | // X returns the x component of the organism's location Point 192 | func (o *Organism) X() int { return o.Location.X } 193 | 194 | // Y returns the y component of the organism's location Point 195 | func (o *Organism) Y() int { return o.Location.Y } 196 | 197 | // GetDecisionTreeCopy returns a copy of an organism's currently-used decision tree 198 | func (o *Organism) GetDecisionTreeCopy() *d.Tree { 199 | return o.decisionTree.CopyTree() 200 | } 201 | 202 | // GetCurrentDecisionTreeLength returns the number of nodes in the organism's currently-used 203 | // decision tree 204 | func (o *Organism) GetCurrentDecisionTreeLength() int { 205 | return o.decisionTree.Size() 206 | } 207 | 208 | // Traits returns an organism's traits 209 | func (o Organism) Traits() Traits { return o.traits } 210 | 211 | // InitialHealth returns the health an organism and its children start life with 212 | func (o Organism) InitialHealth() float64 { return o.traits.SpawnHealth } 213 | 214 | // HealthCostToReproduce returns the health to lose upon spawning a child 215 | func (o Organism) HealthCostToReproduce() float64 { return o.traits.SpawnHealth * -1.0 } 216 | 217 | // MinHealthToSpawn returns the minimum health required for an organism to spawn a child 218 | func (o Organism) MinHealthToSpawn() float64 { return o.traits.MinHealthToSpawn } 219 | 220 | // MinCyclesBetweenSpawns returns the minimum number of cycles needed for an 221 | // organism to spawn 222 | func (o Organism) MinCyclesBetweenSpawns() int { return o.traits.MinCyclesBetweenSpawns } 223 | 224 | // ChanceToMutateDecisionTree returns the chance this organism will give a 225 | // mutated copy of its decision tree to each spawned child 226 | func (o Organism) ChanceToMutateDecisionTree() float64 { return o.traits.ChanceToMutateDecisionTree } 227 | 228 | // Action returns the Organism's currently-chosen action 229 | func (o Organism) Action() d.Action { return o.action } 230 | 231 | // Color returns an organism's color 232 | func (o Organism) Color() color.Color { return o.traits.OrganismColor } 233 | 234 | // MaxSize returns an organism's maximum size 235 | func (o *Organism) MaxSize() float64 { return o.traits.MaxSize } 236 | 237 | func (o *Organism) setDecisionTree(decisionTree *d.Tree) { 238 | if o.decisionTree != nil { 239 | o.decisionTree.SetUsedInCurrentTree(false) 240 | } 241 | o.decisionTree = decisionTree 242 | o.decisionTree.SetUsedInCurrentTree(true) 243 | } 244 | 245 | // ApplyHealthChange adds a value to the organism's health, bounded by 0 and MaxSize 246 | // If new health is greater than the organism's Size, this is updated too. 247 | func (o *Organism) ApplyHealthChange(change float64) { 248 | o.Health += change 249 | if o.Health > o.Size { 250 | // When health increase causes size to increase, increase slowly, not all at once. 251 | difference := o.Health - o.Size 252 | o.Size = math.Min(o.Size+(difference*c.GrowthFactor()), o.traits.MaxSize) 253 | } 254 | o.Health = math.Min(o.Health, o.Size) 255 | } 256 | 257 | func (o *Organism) isFoodAhead() bool { 258 | return o.isFoodAtPoint(o.Location.Add(o.Direction)) 259 | } 260 | 261 | func (o *Organism) isFoodLeft() bool { 262 | return o.isFoodAtPoint(o.Location.Add(o.Direction.Left())) 263 | } 264 | 265 | func (o *Organism) isFoodRight() bool { 266 | return o.isFoodAtPoint(o.Location.Add(o.Direction.Right())) 267 | } 268 | 269 | func (o *Organism) isFoodAtPoint(point utils.Point) bool { 270 | return o.lookupAPI.CheckFoodAtPoint(point, func(f *food.Item, exists bool) bool { 271 | return exists 272 | }) 273 | } 274 | 275 | func (o *Organism) isOrganismAhead() bool { 276 | return o.isOrganismAtPoint(o.Location.Add(o.Direction)) 277 | } 278 | 279 | func (o *Organism) isWallAhead() bool { 280 | return o.isWallAtPoint(o.Location.Add(o.Direction)) 281 | } 282 | 283 | func (o *Organism) isBiggerOrganismAhead() bool { 284 | return o.isBiggerOrganismAtPoint(o.Location.Add(o.Direction)) 285 | } 286 | 287 | func (o *Organism) isRelatedOrganismAhead() bool { 288 | return o.isRelatedOrganismAtPoint(o.Location.Add(o.Direction)) 289 | } 290 | 291 | func (o *Organism) isOrganismLeft() bool { 292 | return o.isOrganismAtPoint(o.Location.Add(o.Direction.Left())) 293 | } 294 | 295 | func (o *Organism) isRelatedOrganismLeft() bool { 296 | return o.isRelatedOrganismAtPoint(o.Location.Add(o.Direction.Left())) 297 | } 298 | 299 | func (o *Organism) isOrganismRight() bool { 300 | return o.isOrganismAtPoint(o.Location.Add(o.Direction.Right())) 301 | } 302 | 303 | func (o *Organism) isRelatedOrganismRight() bool { 304 | return o.isRelatedOrganismAtPoint(o.Location.Add(o.Direction.Right())) 305 | } 306 | 307 | func (o *Organism) isHealthyPhHere() bool { 308 | return o.isPhHealthyAtPoint(o.Location, o.Traits().IdealPh, o.Traits().PhTolerance) 309 | } 310 | 311 | func (o *Organism) isHealthierPhAhead() bool { 312 | return o.isPhHealthierAtPoint(o.Location, o.Location.Add(o.Direction), o.Traits().IdealPh) 313 | } 314 | 315 | func (o *Organism) isBiggerOrganismAtPoint(p utils.Point) bool { 316 | return o.checkOrganismAtPoint(p, func(x *Organism) bool { 317 | return x != nil && x.Size > o.Size 318 | }) 319 | } 320 | 321 | func (o *Organism) isRelatedOrganismAtPoint(p utils.Point) bool { 322 | return o.checkOrganismAtPoint(p, func(x *Organism) bool { 323 | return x != nil && x.OriginalAncestorID == o.OriginalAncestorID 324 | }) 325 | } 326 | 327 | func (o *Organism) isOrganismAtPoint(p utils.Point) bool { 328 | return o.checkOrganismAtPoint(p, func(x *Organism) bool { 329 | return x != nil 330 | }) 331 | } 332 | 333 | func (o *Organism) isWallAtPoint(p utils.Point) bool { 334 | return p.IsWall() 335 | } 336 | 337 | func (o *Organism) checkOrganismAtPoint(p utils.Point, checkFunc OrgCheck) bool { 338 | return o.lookupAPI.CheckOrganismAtPoint(p, checkFunc) 339 | } 340 | 341 | func (o *Organism) isPhHealthyAtPoint(p utils.Point, ideal, tolerance float64) bool { 342 | ph := o.lookupAPI.GetPhAtPoint(p) 343 | return math.Abs(ph-ideal) < tolerance 344 | } 345 | 346 | func (o *Organism) isPhHealthierAtPoint(control, test utils.Point, ideal float64) bool { 347 | controlPh := o.lookupAPI.GetPhAtPoint(control) 348 | testPh := o.lookupAPI.GetPhAtPoint(test) 349 | return math.Abs(testPh-ideal) < math.Abs(controlPh-ideal) 350 | } 351 | 352 | func (o *Organism) canMove() bool { 353 | if o.isWallAhead() { 354 | return false 355 | } 356 | if o.isOrganismAhead() { 357 | return false 358 | } 359 | if o.isFoodAhead() { 360 | return false 361 | } 362 | return true 363 | } 364 | -------------------------------------------------------------------------------- /ux/grid.go: -------------------------------------------------------------------------------- 1 | package ux 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Zebbeni/protozoa/config" 6 | "github.com/Zebbeni/protozoa/decision" 7 | "github.com/Zebbeni/protozoa/food" 8 | "github.com/Zebbeni/protozoa/organism" 9 | "github.com/Zebbeni/protozoa/resources" 10 | "github.com/Zebbeni/protozoa/simulation" 11 | "github.com/Zebbeni/protozoa/utils" 12 | "github.com/hajimehoshi/ebiten/v2" 13 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 14 | "github.com/hajimehoshi/ebiten/v2/text" 15 | "github.com/lucasb-eyer/go-colorful" 16 | "math" 17 | ) 18 | 19 | type size int 20 | type mode int 21 | 22 | const ( 23 | sizeSmall size = iota 24 | sizeMedium 25 | sizeLarge 26 | sizeFill 27 | sizeBox 28 | ) 29 | 30 | // use separate constant group to ensure orgsPhMode starts at 0 31 | const ( 32 | orgsPhMode mode = iota 33 | organismsOnlyMode 34 | phEffectsOnlyMode 35 | phOnlyMode 36 | ) 37 | 38 | const ( 39 | selectOldest mode = iota 40 | selectMostChildren 41 | selectMostTraveled 42 | selectManual 43 | ) 44 | 45 | const ( 46 | phMaxHue = 120.0 47 | ) 48 | 49 | var ( 50 | squareImgSmall, squareImgMedium, squareImgLarge, squareImgFill, squareImgBox *ebiten.Image 51 | 52 | foodColor = colorful.HSLuv(120, 0.2, 0.25) 53 | wallColor = colorful.HSLuv(60, 0.25, 0.1) 54 | attackColor = colorful.HSLuv(0.0, 255.0, 1.0) 55 | selectColor = colorful.HSLuv(0.0, 255.0, 1.0) 56 | hoverColor = colorful.HSLuv(0.0, 0, 0.7) 57 | selectionInfoColor = colorful.HSLuv(0.0, 0, 1.0) 58 | viewModes = []mode{orgsPhMode, organismsOnlyMode, phEffectsOnlyMode, phOnlyMode} 59 | selectModes = []mode{selectOldest, selectMostChildren, selectMostTraveled, selectManual} 60 | viewModeNames = map[mode]string{ 61 | orgsPhMode: "ORGANISMS & PH", 62 | organismsOnlyMode: "ORGANISMS ONLY", 63 | phEffectsOnlyMode: "ORGANISM PH EFFECTS", 64 | phOnlyMode: "PH ONLY", 65 | } 66 | selectModeNames = map[mode]string{ 67 | selectOldest: "OLDEST", 68 | selectMostChildren: "MOST CHILDREN", 69 | selectMostTraveled: "MOST TRAVELED", 70 | selectManual: "MANUAL SELECT", 71 | } 72 | ) 73 | 74 | type Grid struct { 75 | simulation *simulation.Simulation 76 | 77 | previousEnvImage *ebiten.Image 78 | previousWallsImage *ebiten.Image 79 | previousFoodImage *ebiten.Image 80 | previousOrgsImage *ebiten.Image 81 | 82 | mouseHoverLocation utils.Point 83 | mouseOnGrid bool 84 | doRefresh bool 85 | viewMode mode 86 | selectMode mode 87 | } 88 | 89 | func NewGrid(simulation *simulation.Simulation) *Grid { 90 | g := &Grid{ 91 | simulation: simulation, 92 | previousWallsImage: newBlankLayer(), 93 | previousEnvImage: newBlankLayer(), 94 | previousFoodImage: newBlankLayer(), 95 | previousOrgsImage: newBlankLayer(), 96 | doRefresh: true, 97 | viewMode: orgsPhMode, 98 | selectMode: selectOldest, 99 | } 100 | loadOrganismImages() 101 | return g 102 | } 103 | 104 | func loadOrganismImages() { 105 | squareImgSmall = resources.SquareSmall 106 | squareImgMedium = resources.SquareMedium 107 | squareImgLarge = resources.SquareLarge 108 | squareImgFill = resources.SquareFill 109 | squareImgBox = resources.SquareBox 110 | } 111 | 112 | // Render draws all organisms and food on the simulation grid 113 | func (g *Grid) Render() *ebiten.Image { 114 | envImage := newBlankLayer() 115 | wallsImage := newBlankLayer() 116 | foodImage := newBlankLayer() 117 | orgsImage := newBlankLayer() 118 | selImage := newBlankLayer() 119 | gridImage := newBlankLayer() 120 | 121 | g.renderWalls(wallsImage, g.doRefresh) 122 | g.renderEnvironment(envImage, g.doRefresh) 123 | g.renderFood(foodImage, g.doRefresh) 124 | g.renderOrganisms(orgsImage, g.doRefresh) 125 | g.renderSelections(selImage) 126 | 127 | g.previousWallsImage = wallsImage 128 | g.previousEnvImage = envImage 129 | g.previousFoodImage = foodImage 130 | g.previousOrgsImage = orgsImage 131 | 132 | if g.viewMode == orgsPhMode || g.viewMode == phOnlyMode { 133 | gridImage.DrawImage(envImage, nil) 134 | } 135 | 136 | gridImage.DrawImage(wallsImage, nil) 137 | 138 | if g.viewMode != phOnlyMode { 139 | gridImage.DrawImage(foodImage, nil) 140 | gridImage.DrawImage(orgsImage, nil) 141 | } 142 | 143 | gridImage.DrawImage(selImage, nil) 144 | 145 | g.doRefresh = false 146 | 147 | return gridImage 148 | } 149 | 150 | func (g *Grid) renderEnvironment(envImage *ebiten.Image, refresh bool) { 151 | if refresh { 152 | phMap := g.simulation.GetPhMap() 153 | for x := range phMap { 154 | for y := range phMap[x] { 155 | g.renderPhValue(envImage, x, y, phMap[x][y]) 156 | } 157 | } 158 | } else { 159 | envImage.DrawImage(g.previousEnvImage, nil) 160 | updatedPoints := g.simulation.GetUpdatedPhPoints() 161 | for _, point := range updatedPoints { 162 | // clear square to be updated 163 | phVal := g.simulation.GetPhAtPoint(point) 164 | g.renderPhValue(envImage, point.X, point.Y, phVal) 165 | } 166 | } 167 | } 168 | 169 | func (g *Grid) renderWalls(wallsImage *ebiten.Image, refresh bool) { 170 | if refresh { 171 | wallPoints := g.simulation.GetWalls() 172 | for _, wallPoint := range wallPoints { 173 | g.renderWall(wallsImage, wallPoint) 174 | } 175 | } else { 176 | wallsImage.DrawImage(g.previousWallsImage, nil) 177 | } 178 | } 179 | 180 | func (g *Grid) renderPhValue(envImage *ebiten.Image, gridX, gridY int, phVal float64) { 181 | x := float64(gridX) * float64(config.GridUnitSize()) 182 | y := float64(gridY) * float64(config.GridUnitSize()) 183 | hue := phMaxHue - (phMaxHue * phVal / config.MaxPh()) 184 | sat := math.Abs(phVal-((config.MaxPh()+config.MinPh())/2.0)) / (config.MaxPh() - config.MinPh()) 185 | light := 0.5 + (0.5 * math.Sin(math.Pi*(sat-0.5))) 186 | col := colorful.HSLuv(hue, sat, light) 187 | g.drawSquare(envImage, x, y, sizeFill, col) 188 | } 189 | 190 | func (g *Grid) renderFood(foodImage *ebiten.Image, refresh bool) { 191 | if refresh { 192 | items := g.simulation.GetFoodItems() 193 | for _, item := range items { 194 | g.renderFoodItem(item, foodImage) 195 | } 196 | } else { 197 | foodImage.DrawImage(g.previousFoodImage, nil) 198 | updatedPoints := g.simulation.GetUpdatedFoodPoints() 199 | for _, point := range updatedPoints { 200 | // clear square to be updated 201 | x, y := point.X*config.GridUnitSize(), point.Y*config.GridUnitSize() 202 | g.clearSquare(foodImage, float64(x), float64(y)) 203 | 204 | if item, exists := g.simulation.GetFoodAtPoint(point); exists { 205 | g.renderFoodItem(item, foodImage) 206 | } 207 | } 208 | } 209 | } 210 | 211 | func (g *Grid) renderOrganisms(organismsImage *ebiten.Image, refresh bool) { 212 | if refresh { 213 | organismInfo := g.simulation.GetAllOrganismInfo() 214 | for _, info := range organismInfo { 215 | g.renderOrganism(info, organismsImage) 216 | } 217 | } else { 218 | organismsImage.DrawImage(g.previousOrgsImage, nil) 219 | updatedPoints := g.simulation.GetUpdatedOrganismPoints() 220 | for _, point := range updatedPoints { 221 | // clear square to be updated 222 | x, y := point.X*config.GridUnitSize(), point.Y*config.GridUnitSize() 223 | g.clearSquare(organismsImage, float64(x), float64(y)) 224 | 225 | if info := g.simulation.GetOrganismInfoAtPoint(point); info != nil { 226 | g.renderOrganism(info, organismsImage) 227 | } 228 | } 229 | } 230 | } 231 | 232 | func (g *Grid) renderSelections(selectionsImage *ebiten.Image) { 233 | if g.mouseOnGrid { 234 | infoColor := hoverColor 235 | infoText := fmt.Sprintf("PH: %2.1f", g.simulation.GetPhAtPoint(g.mouseHoverLocation)) 236 | if info := g.simulation.GetOrganismInfoAtPoint(g.mouseHoverLocation); info != nil { 237 | infoText += fmt.Sprintf("\nORG: %d", info.ID) 238 | infoText += fmt.Sprintf("\nSIZE: %.0f", info.Size) 239 | infoColor = info.Color 240 | } else { 241 | if foodItem, exists := g.simulation.GetFoodAtPoint(g.mouseHoverLocation); exists { 242 | infoText += fmt.Sprintf("\nFOOD: %d", foodItem.Value) 243 | } 244 | } 245 | infoText += fmt.Sprintf("\nPOINT: %v", g.mouseHoverLocation) 246 | 247 | g.renderSelection(g.mouseHoverLocation, selectionsImage, infoColor) 248 | g.renderSelectionText(g.mouseHoverLocation, selectionsImage, infoText, selectionInfoColor) 249 | g.renderViewModeName(selectionsImage) 250 | } 251 | 252 | if info := g.simulation.GetOrganismInfoByID(g.simulation.GetSelected()); info != nil { 253 | g.renderSelection(info.Location, selectionsImage, selectColor) 254 | } 255 | } 256 | 257 | func newBlankLayer() *ebiten.Image { 258 | return ebiten.NewImage(config.GridWidth(), config.GridHeight()) 259 | } 260 | 261 | // ChangeViewMode switches to the next mode listed in viewModes 262 | func (g *Grid) ChangeViewMode() { 263 | g.viewMode = viewModes[(int(g.viewMode)+1)%len(viewModes)] 264 | g.doRefresh = true 265 | } 266 | 267 | // UpdateAutoSelect switches to the next auto select mode listed in selectModes 268 | func (g *Grid) UpdateAutoSelect() { 269 | // cycle among all but the last selectMode, which is manual. 270 | // Manual selection his is switched to by clicking an organism 271 | g.selectMode = selectModes[(int(g.selectMode)+1)%(len(selectModes)-1)] 272 | g.doRefresh = true 273 | } 274 | 275 | func (g *Grid) SetManualSelection() { 276 | g.selectMode = selectManual 277 | } 278 | 279 | func (g *Grid) MouseHover(point utils.Point, onGrid bool) { 280 | g.mouseHoverLocation = point 281 | g.mouseOnGrid = onGrid 282 | } 283 | 284 | // renderSelection draws a square around a single item on the grid 285 | func (g *Grid) renderSelection(point utils.Point, img *ebiten.Image, col colorful.Color) { 286 | x, y := float64(point.X*config.GridUnitSize()), float64(point.Y*config.GridUnitSize()) 287 | ebitenutil.DrawLine(img, x-2, y-2, x+float64(config.GridUnitSize())+3, y-2, col) // top 288 | ebitenutil.DrawLine(img, x-2, y-2, x-2, y+float64(config.GridUnitSize())+3, col) // left 289 | ebitenutil.DrawLine(img, x-2, y+float64(config.GridUnitSize())+3, x+float64(config.GridUnitSize())+3, y+float64(config.GridUnitSize())+3, col) // bottom 290 | ebitenutil.DrawLine(img, x+float64(config.GridUnitSize())+3, y-2, x+float64(config.GridUnitSize())+3, y+float64(config.GridUnitSize())+3, col) // right 291 | } 292 | 293 | func (g *Grid) renderSelectionText(point utils.Point, img *ebiten.Image, message string, col colorful.Color) { 294 | xPadding := 10 295 | bounds := text.BoundString(resources.FontSourceCodePro10, message) 296 | x := xPadding + config.GridUnitSize() + (point.X * config.GridUnitSize()) 297 | y := point.Y * config.GridUnitSize() 298 | if x+bounds.Dx() > config.GridWidth() { 299 | x = (point.X * config.GridUnitSize()) - xPadding - bounds.Dx() 300 | } 301 | text.Draw(img, message, resources.FontSourceCodePro10, x, y, col) 302 | } 303 | 304 | func (g *Grid) renderViewModeName(img *ebiten.Image) { 305 | xPadding := 10 306 | yPadding := 20 307 | x := xPadding 308 | y := yPadding 309 | info := fmt.Sprintf("VIEW MODE: %s\nSELECTED: %s", viewModeNames[g.viewMode], selectModeNames[g.selectMode]) 310 | text.Draw(img, info, resources.FontSourceCodePro10, x, y, selectionInfoColor) 311 | } 312 | 313 | // renderFoodItem draws a food item to the given image 314 | func (g *Grid) renderFoodItem(item *food.Item, img *ebiten.Image) { 315 | x := float64(item.Point.X) * float64(config.GridUnitSize()) 316 | y := float64(item.Point.Y) * float64(config.GridUnitSize()) 317 | 318 | value := float64(item.Value) 319 | foodSize := sizeSmall 320 | if value < float64(config.MaxFoodValue())*0.4375 { 321 | foodSize = sizeSmall 322 | } else if value < float64(config.MaxFoodValue())*0.8125 { 323 | foodSize = sizeMedium 324 | } else { 325 | foodSize = sizeLarge 326 | } 327 | 328 | g.drawSquare(img, x, y, foodSize, foodColor) 329 | } 330 | 331 | // renderWall draws a wall icon to the given image 332 | func (g *Grid) renderWall(wallsImage *ebiten.Image, point utils.Point) { 333 | x := float64(point.X) * float64(config.GridUnitSize()) 334 | y := float64(point.Y) * float64(config.GridUnitSize()) 335 | 336 | g.drawSquare(wallsImage, x, y, sizeBox, wallColor) 337 | } 338 | 339 | // renderOrganism draws an organism to the given image 340 | func (g *Grid) renderOrganism(info *organism.Info, img *ebiten.Image) { 341 | point := info.Location.Times(config.GridUnitSize()) 342 | x, y := float64(point.X), float64(point.Y) 343 | 344 | organismSize := sizeSmall 345 | if info.Size < config.MaximumMaxSize()*0.4375 { 346 | organismSize = sizeSmall 347 | } else if info.Size < config.MaximumMaxSize()*0.8125 { 348 | organismSize = sizeMedium 349 | } else { 350 | organismSize = sizeLarge 351 | } 352 | 353 | organismColor := info.Color 354 | 355 | if g.viewMode == phEffectsOnlyMode { 356 | maxEffect := config.MaxOrganismPhGrowthEffect() * info.Size 357 | spectrumValue := (info.Size*info.PhEffect + maxEffect) / (2 * maxEffect) 358 | hue := phMaxHue - (phMaxHue * spectrumValue) 359 | sat := 1.0 + math.Abs(spectrumValue-0.5) 360 | light := 0.25 + math.Abs(spectrumValue-0.5) 361 | organismColor = colorful.HSLuv(hue, sat, light) 362 | } 363 | 364 | if info.Action == decision.ActAttack { 365 | organismColor = attackColor 366 | } 367 | 368 | g.drawSquare(img, x, y, organismSize, organismColor) 369 | } 370 | 371 | func (g *Grid) drawSquare(img *ebiten.Image, x, y float64, sz size, col colorful.Color) { 372 | var squareImg *ebiten.Image 373 | switch sz { 374 | case sizeSmall: 375 | squareImg = squareImgSmall 376 | break 377 | case sizeMedium: 378 | squareImg = squareImgMedium 379 | break 380 | case sizeLarge: 381 | squareImg = squareImgLarge 382 | break 383 | case sizeFill: 384 | squareImg = squareImgFill 385 | break 386 | case sizeBox: 387 | squareImg = squareImgBox 388 | break 389 | } 390 | 391 | op := &ebiten.DrawImageOptions{} 392 | op.GeoM.Translate(x, y) 393 | op.ColorM.Translate(col.R, col.G, col.B, 0) 394 | img.DrawImage(squareImg, op) 395 | } 396 | 397 | func (g *Grid) clearSquare(img *ebiten.Image, x, y float64) { 398 | squareImg := squareImgFill 399 | op := &ebiten.DrawImageOptions{} 400 | op.GeoM.Translate(x, y) 401 | op.ColorM.Translate(0, 0, 0, 1.0) 402 | op.CompositeMode = ebiten.CompositeModeDestinationOut 403 | 404 | img.DrawImage(squareImg, op) 405 | } 406 | -------------------------------------------------------------------------------- /manager/organism.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | "sync" 8 | "time" 9 | 10 | c "github.com/Zebbeni/protozoa/config" 11 | d "github.com/Zebbeni/protozoa/decision" 12 | "github.com/Zebbeni/protozoa/food" 13 | "github.com/Zebbeni/protozoa/organism" 14 | "github.com/Zebbeni/protozoa/utils" 15 | ) 16 | 17 | // OrganismManager contains 2D array of booleans showing if organism present 18 | type OrganismManager struct { 19 | api organism.API 20 | requestManager RequestManager 21 | 22 | organisms map[int]*organism.Organism 23 | organismIDGrid [][]int 24 | totalOrganismsCreated int 25 | 26 | organismIds []int 27 | 28 | oldestId int 29 | oldestAge int 30 | mostChildrenId int 31 | mostChildren int 32 | mostTraveledId int 33 | mostTraveledDist int 34 | 35 | originalAncestors []int 36 | originalAncestorColors map[int]color.Color // all original ancestor IDs with at least one descendant 37 | populationHistory map[int]map[int]int32 // cycle : ancestorId : livingDescendantsCount 38 | 39 | UpdateDuration, ResolveDuration time.Duration 40 | 41 | ancestorMutex sync.RWMutex 42 | gridMutex sync.RWMutex 43 | organismMutex sync.RWMutex 44 | } 45 | 46 | // NewOrganismManager creates all Organisms and updates grid 47 | func NewOrganismManager(api organism.API) *OrganismManager { 48 | grid := initializeGrid() 49 | organisms := make(map[int]*organism.Organism) 50 | manager := &OrganismManager{ 51 | api: api, 52 | requestManager: RequestManager{}, 53 | organismIDGrid: grid, 54 | organisms: organisms, 55 | organismIds: make([]int, 0, c.MaxOrganisms()), 56 | originalAncestorColors: make(map[int]color.Color), 57 | populationHistory: make(map[int]map[int]int32), 58 | } 59 | manager.InitializeOrganisms(c.InitialOrganisms()) 60 | return manager 61 | } 62 | 63 | func (m *OrganismManager) InitializeOrganisms(count int) { 64 | for i := 0; i < count; i++ { 65 | m.SpawnRandomOrganism() 66 | } 67 | } 68 | 69 | // Update walks through decision tree of each organism and applies the 70 | // chosen action to the organism, the grid, and the environment 71 | func (m *OrganismManager) Update() { 72 | m.requestManager.ClearMaps() 73 | m.organismIds = make([]int, 0, c.MaxOrganisms()) 74 | 75 | m.resetInterestingStats() 76 | 77 | m.updateOrganismActions() 78 | m.resolveOrganismActions() 79 | 80 | m.updateHistory() 81 | } 82 | 83 | func (m *OrganismManager) updateOrganismActions() { 84 | start := time.Now() 85 | 86 | m.organismMutex.RLock() 87 | orgsToUpdate := make(chan int, len(m.organisms)) 88 | m.organismMutex.RUnlock() 89 | 90 | numWorkers := 8 91 | var wg sync.WaitGroup 92 | wg.Add(numWorkers) 93 | 94 | for i := 0; i < numWorkers; i++ { 95 | go func() { 96 | for k := range orgsToUpdate { 97 | m.organismMutex.RLock() 98 | o := m.organisms[k] 99 | m.organismMutex.RUnlock() 100 | 101 | m.updateOrganismAction(o) 102 | } 103 | wg.Done() 104 | }() 105 | } 106 | 107 | for k := range m.organisms { 108 | orgsToUpdate <- k 109 | } 110 | close(orgsToUpdate) 111 | 112 | // wait for all worker threads to call Done() 113 | wg.Wait() 114 | 115 | m.UpdateDuration = time.Since(start) 116 | } 117 | 118 | func (m *OrganismManager) resetInterestingStats() { 119 | m.oldestId = -1 120 | m.oldestAge = -1 121 | m.mostChildrenId = -1 122 | m.mostChildren = -1 123 | m.mostTraveledId = -1 124 | m.mostTraveledDist = -1 125 | } 126 | 127 | func (m *OrganismManager) updateInterestingStats(o *organism.Organism) { 128 | if o.Age > m.oldestAge || (o.Age == m.oldestAge && o.ID < m.oldestId) { 129 | m.oldestAge = o.Age 130 | m.oldestId = o.ID 131 | } 132 | if o.Children > m.mostChildren || (o.Children == m.mostChildren && o.ID < m.mostChildrenId) { 133 | m.mostChildren = o.Children 134 | m.mostChildrenId = o.ID 135 | } 136 | if o.TraveledDist > m.mostTraveledDist || o.TraveledDist == m.mostTraveledDist && o.ID < m.mostTraveledId { 137 | m.mostTraveledDist = o.TraveledDist 138 | m.mostTraveledId = o.ID 139 | } 140 | } 141 | 142 | func (m *OrganismManager) resolveOrganismActions() { 143 | start := time.Now() 144 | 145 | orgsToResolve := make(chan *organism.Organism, len(m.organismIds)) 146 | 147 | numWorkers := 8 148 | var wg sync.WaitGroup 149 | wg.Add(numWorkers) 150 | 151 | for i := 0; i < numWorkers; i++ { 152 | go func() { 153 | for o := range orgsToResolve { 154 | m.resolveOrganismAction(o) 155 | } 156 | wg.Done() 157 | }() 158 | } 159 | 160 | for _, id := range m.organismIds { 161 | m.organismMutex.RLock() 162 | orgsToResolve <- m.organisms[id] 163 | m.organismMutex.RUnlock() 164 | } 165 | close(orgsToResolve) 166 | // wait for all worker threads to call Done() 167 | wg.Wait() 168 | 169 | m.ResolveDuration = time.Since(start) 170 | } 171 | 172 | // updateHistory updates the population map for all living organisms 173 | func (m *OrganismManager) updateHistory() { 174 | cycle := m.api.Cycle() 175 | if cycle%c.PopulationUpdateInterval() != 0 { 176 | return 177 | } 178 | 179 | populationMap := make(map[int]int32) 180 | 181 | for _, o := range m.organisms { 182 | if _, ok := populationMap[o.OriginalAncestorID]; !ok { 183 | populationMap[o.OriginalAncestorID] = 0 184 | } 185 | populationMap[o.OriginalAncestorID]++ 186 | } 187 | 188 | m.populationHistory[cycle] = populationMap 189 | } 190 | 191 | func (m *OrganismManager) updateRequestMap(o *organism.Organism) { 192 | switch o.Action() { 193 | case d.ActEat: 194 | m.addFoodRequest(o) 195 | case d.ActMove: 196 | m.addMoveRequest(o) 197 | case d.ActSpawn: 198 | m.addSpawnRequest(o) 199 | case d.ActAttack: 200 | m.addAttackRequest(o) 201 | case d.ActFeed: 202 | m.addFeedRequest(o) 203 | default: 204 | return 205 | } 206 | } 207 | 208 | func (m *OrganismManager) addAttackRequest(o *organism.Organism) { 209 | effect := m.calculateAttackEffect(o) 210 | target := o.Location.Add(o.Direction) 211 | m.requestManager.AddHealthEffectRequest(target, effect) 212 | } 213 | 214 | func (m *OrganismManager) addFeedRequest(o *organism.Organism) { 215 | // the feed effect constant is negative so multiply by -1 to 216 | // get a positive health benefit for the beneficiary organism 217 | effect := -1 * m.calculateFeedEffect(o) 218 | target := o.Location.Add(o.Direction) 219 | m.requestManager.AddHealthEffectRequest(target, effect) 220 | } 221 | 222 | func (m *OrganismManager) addSpawnRequest(o *organism.Organism) { 223 | target, ok := m.getChildSpawnLocation(o) 224 | if ok { 225 | m.requestManager.AddPositionRequest(target, o.ID) 226 | } 227 | } 228 | 229 | // determine the new position required for a move action add 1 to the number of 230 | // requests for this position in positionRequests 231 | func (m *OrganismManager) addMoveRequest(o *organism.Organism) { 232 | target := o.Location.Add(o.Direction) 233 | 234 | // Only make request if empty, to avoid complications resolving it later 235 | if empty := m.isGridLocationEmpty(target); empty { 236 | m.requestManager.AddPositionRequest(target, o.ID) 237 | } 238 | } 239 | 240 | // calculate the amount of food the given organism requests to eat at a target 241 | // location. Add this to the food item stored for this location representing 242 | // the total eat requests made here. 243 | func (m *OrganismManager) addFoodRequest(o *organism.Organism) { 244 | target := o.Location.Add(o.Direction) 245 | value := int(math.Ceil(m.calculateValueToEat(o, target))) 246 | m.requestManager.AddFoodRequest(target, value) 247 | } 248 | 249 | // GetHistory returns the full population history of all original ancestors as a 250 | // map of cycles to maps of ancestorIDs to the living descendants at that time 251 | func (m *OrganismManager) GetHistory() map[int]map[int]int32 { 252 | return m.populationHistory 253 | } 254 | 255 | // GetAncestorColors returns a map all original ancestor IDs to their color 256 | func (m *OrganismManager) GetAncestorColors() map[int]color.Color { 257 | return m.originalAncestorColors 258 | } 259 | 260 | // GetAncestors returns a list of all original ancestor IDs 261 | func (m *OrganismManager) GetAncestors() []int { 262 | return m.originalAncestors 263 | } 264 | 265 | func (m *OrganismManager) addUpdatedPoint(point utils.Point) { 266 | m.api.AddOrganismUpdate(point) 267 | } 268 | 269 | func (m *OrganismManager) applyOrganismPhGrowthEffect(o *organism.Organism) { 270 | m.api.AddPhChangeAtPoint(o.Location, o.Traits().PhGrowthEffect*o.Size) 271 | } 272 | 273 | func (m *OrganismManager) updateOrganismAction(o *organism.Organism) { 274 | // if previous action was attack, allow the screen to render white 275 | if o.Action() == d.ActAttack { 276 | m.addUpdatedPoint(o.Location) 277 | } 278 | o.UpdateStats() 279 | o.UpdateAction() 280 | m.updateRequestMap(o) 281 | m.addToOrganismIds(o) 282 | } 283 | 284 | func (m *OrganismManager) addToOrganismIds(o *organism.Organism) { 285 | m.organismMutex.Lock() 286 | m.organismIds = append(m.organismIds, o.ID) 287 | m.organismMutex.Unlock() 288 | } 289 | 290 | func (m *OrganismManager) resolveOrganismAction(o *organism.Organism) { 291 | if o == nil || o.Age == 0 { 292 | return 293 | } 294 | m.applyCycleHealthChanges(o) 295 | m.applyAction(o) 296 | m.removeIfDead(o) 297 | m.updateInterestingStats(o) 298 | } 299 | 300 | // SpawnRandomOrganism creates an Organism with random position. 301 | // 302 | // Checks random positions on the grid until it finds an empty one. Calls 303 | // NewOrganism to initialize decision tree, other random attributes. 304 | func (m *OrganismManager) SpawnRandomOrganism() { 305 | if spawnPoint, found := m.getRandomSpawnLocation(); found { 306 | id := m.generateId() 307 | o := organism.NewRandom(id, spawnPoint, m.api) 308 | m.registerNewOrganism(o, id) 309 | } 310 | } 311 | 312 | // SpawnChildOrganism creates a new organism near an existing 'parent' organism 313 | // with a copy of its parent's node library. (No organism created if no room or 314 | // if more than 1 organism made a request to spawn and/or move into the desired 315 | // location. 316 | // Returns true / false depending on whether a child was actually spawned. 317 | func (m *OrganismManager) SpawnChildOrganism(parent *organism.Organism) bool { 318 | spawnPoint, found := m.getChildSpawnLocation(parent) 319 | if found == false { 320 | return false 321 | } 322 | if m.isMatchingPositionRequest(spawnPoint, parent.ID) == false { 323 | return false 324 | } 325 | id := m.generateId() 326 | o := parent.NewChild(id, spawnPoint, m.api) 327 | m.registerNewOrganism(o, id) 328 | m.addToOriginalAncestors(parent) 329 | return true 330 | } 331 | 332 | // return true iff the request id stored in the position requests matches 333 | // the id of the organism checking 334 | func (m *OrganismManager) isMatchingPositionRequest(p utils.Point, id int) bool { 335 | requestId := m.requestManager.GetPositionRequest(p) 336 | return id == requestId 337 | } 338 | 339 | func (m *OrganismManager) generateId() int { 340 | m.organismMutex.Lock() 341 | defer m.organismMutex.Unlock() 342 | 343 | m.totalOrganismsCreated++ 344 | return m.totalOrganismsCreated 345 | } 346 | 347 | func (m *OrganismManager) registerNewOrganism(o *organism.Organism, index int) { 348 | m.addUpdatedPoint(o.Location) 349 | 350 | m.gridMutex.Lock() 351 | m.organismMutex.Lock() 352 | 353 | m.organisms[index] = o 354 | m.organismIDGrid[o.X()][o.Y()] = index 355 | 356 | m.gridMutex.Unlock() 357 | m.organismMutex.Unlock() 358 | } 359 | 360 | func (m *OrganismManager) addToOriginalAncestors(o *organism.Organism) { 361 | m.ancestorMutex.RLock() 362 | _, ok := m.originalAncestorColors[o.ID] 363 | m.ancestorMutex.RUnlock() 364 | if ok { 365 | return 366 | } 367 | 368 | m.ancestorMutex.Lock() 369 | m.originalAncestorColors[o.ID] = o.Color() 370 | m.originalAncestors = append(m.originalAncestors, o.ID) 371 | m.ancestorMutex.Unlock() 372 | } 373 | 374 | // returns a random point and whether it is empty 375 | func (m *OrganismManager) getRandomSpawnLocation() (utils.Point, bool) { 376 | point := utils.GetRandomPoint(c.GridUnitsWide(), c.GridUnitsHigh()) 377 | isEmpty := m.isGridLocationEmpty(point) 378 | return point, isEmpty 379 | } 380 | 381 | func (m *OrganismManager) getChildSpawnLocation(parent *organism.Organism) (utils.Point, bool) { 382 | var point utils.Point 383 | direction := parent.Direction 384 | for i := 0; i < 4; i++ { 385 | direction = direction.Left() 386 | point = parent.Location.Add(direction) 387 | 388 | empty := m.isGridLocationEmpty(point) 389 | if empty { 390 | return point, true 391 | } 392 | } 393 | 394 | return point, false 395 | } 396 | 397 | func initializeGrid() [][]int { 398 | grid := make([][]int, c.GridUnitsWide()) 399 | for r := 0; r < c.GridUnitsWide(); r++ { 400 | grid[r] = make([]int, c.GridUnitsHigh()) 401 | } 402 | for x := 0; x < c.GridUnitsWide(); x++ { 403 | for y := 0; y < c.GridUnitsHigh(); y++ { 404 | grid[x][y] = -1 405 | } 406 | } 407 | return grid 408 | } 409 | 410 | func (m *OrganismManager) isGridLocationEmpty(point utils.Point) bool { 411 | return !point.IsWall() && !m.isFoodAtLocation(point) && !m.isOrganismAtLocation(point) 412 | } 413 | 414 | func (m *OrganismManager) isFoodAtLocation(point utils.Point) bool { 415 | return m.api.CheckFoodAtPoint(point, func(_ *food.Item, exists bool) bool { 416 | return exists 417 | }) 418 | } 419 | 420 | func (m *OrganismManager) isOrganismAtLocation(point utils.Point) bool { 421 | m.gridMutex.RLock() 422 | id := m.organismIDGrid[point.X][point.Y] 423 | m.gridMutex.RUnlock() 424 | 425 | return id != -1 426 | } 427 | 428 | func (m *OrganismManager) getOrganismAt(point utils.Point) *organism.Organism { 429 | if id, exists := m.getOrganismIDAt(point); exists { 430 | index := id 431 | 432 | m.organismMutex.RLock() 433 | defer m.organismMutex.RUnlock() 434 | 435 | return m.organisms[index] 436 | } 437 | return nil 438 | } 439 | 440 | func (m *OrganismManager) getOrganismIDAt(point utils.Point) (int, bool) { 441 | m.gridMutex.RLock() 442 | id := m.organismIDGrid[point.X][point.Y] 443 | m.gridMutex.RUnlock() 444 | 445 | return id, id != -1 446 | } 447 | 448 | // CheckOrganismAtPoint returns the result of running a check against any Organism 449 | // found at a given Point. 450 | func (m *OrganismManager) CheckOrganismAtPoint(point utils.Point, checkFunc organism.OrgCheck) bool { 451 | return checkFunc(m.getOrganismAt(point)) 452 | } 453 | 454 | // GetOrganismInfoAtPoint returns the Organism Info at the given point (nil if none) 455 | func (m *OrganismManager) GetOrganismInfoAtPoint(point utils.Point) *organism.Info { 456 | if id, found := m.getOrganismIDAt(point); found { 457 | m.organismMutex.RLock() 458 | defer m.organismMutex.RUnlock() 459 | 460 | if o, ok := m.organisms[id]; ok { 461 | return o.Info() 462 | } 463 | } 464 | return nil 465 | } 466 | 467 | // GetOrganismDecisionTreeByID returns a copy of the currently-used decision tree of the 468 | // given organism (nil if no organism found) 469 | func (m *OrganismManager) GetOrganismDecisionTreeByID(id int) *d.Tree { 470 | m.organismMutex.RLock() 471 | defer m.organismMutex.RUnlock() 472 | 473 | if o, ok := m.organisms[id]; ok { 474 | return o.GetDecisionTreeCopy() 475 | } 476 | return nil 477 | } 478 | 479 | // GetOrganismInfoByID returns the Organism Info for a given Organism ID. (nil if not found) 480 | func (m *OrganismManager) GetOrganismInfoByID(id int) *organism.Info { 481 | m.organismMutex.RLock() 482 | defer m.organismMutex.RUnlock() 483 | 484 | if o, found := m.organisms[id]; found { 485 | return o.Info() 486 | } 487 | return nil 488 | } 489 | 490 | // GetOrganismTraitsByID returns the Organism Traits for a given Organism ID and whether it was 491 | // successfully found 492 | func (m *OrganismManager) GetOrganismTraitsByID(id int) (organism.Traits, bool) { 493 | m.organismMutex.RLock() 494 | defer m.organismMutex.RUnlock() 495 | 496 | if o, found := m.organisms[id]; found { 497 | return o.Traits(), true 498 | } 499 | return organism.Traits{}, false 500 | } 501 | 502 | // GetOldestId returns the id of the oldest living organism 503 | func (m *OrganismManager) GetOldestId() int { 504 | return m.oldestId 505 | } 506 | 507 | // GetMostChildrenId returns the id of the most traveled organism 508 | func (m *OrganismManager) GetMostChildrenId() int { 509 | return m.mostChildrenId 510 | } 511 | 512 | // GetMostTraveledId returns the id of the most traveled organism 513 | func (m *OrganismManager) GetMostTraveledId() int { 514 | return m.mostTraveledId 515 | } 516 | 517 | // OrganismCount returns the current number of organisms alive in the simulation 518 | func (m *OrganismManager) OrganismCount() int { 519 | m.organismMutex.RLock() 520 | defer m.organismMutex.RUnlock() 521 | 522 | return len(m.organisms) 523 | } 524 | 525 | // DeadCount returns the total number of organisms that have died in the simulation 526 | func (m *OrganismManager) DeadCount() int { 527 | return m.totalOrganismsCreated - len(m.organisms) 528 | } 529 | 530 | func (m *OrganismManager) applyAction(o *organism.Organism) { 531 | switch o.Action() { 532 | case d.ActChemosynthesis: 533 | m.applyChemosynthesis(o) 534 | break 535 | case d.ActAttack: 536 | m.applyAttack(o) 537 | break 538 | case d.ActFeed: 539 | m.applyFeed(o) 540 | break 541 | case d.ActEat: 542 | m.applyEat(o) 543 | break 544 | case d.ActMove: 545 | m.applyMove(o) 546 | break 547 | case d.ActTurnLeft: 548 | m.applyLeftTurn(o) 549 | break 550 | case d.ActTurnRight: 551 | m.applyRightTurn(o) 552 | break 553 | case d.ActSpawn: 554 | m.applySpawn(o) 555 | break 556 | } 557 | } 558 | 559 | func (m *OrganismManager) applyCycleHealthChanges(o *organism.Organism) { 560 | decisionsEffect := c.HealthChangePerDecisionTreeNode() * float64(o.GetCurrentDecisionTreeLength()) 561 | phEffect := 0.0 562 | // Subtract health if organism is too far away from its ideal ph 563 | phDist := math.Abs(o.Traits().IdealPh - m.api.GetPhAtPoint(o.Location)) 564 | if phDist > o.Traits().PhTolerance { 565 | phEffect = (phDist - o.Traits().PhTolerance) * c.HealthChangePerUnhealthyPh() 566 | } 567 | // Add effects due to feeding and/or attack (not related to organism size) 568 | healthEffects := m.requestManager.GetHealthEffects(o.Location) 569 | m.applyHealthChange(o, o.Size*(decisionsEffect+phEffect)+healthEffects) 570 | } 571 | 572 | // add a positive health change if organism attempts chemosynthesis in a 573 | // favorable ph environment 574 | func (m *OrganismManager) applyChemosynthesis(o *organism.Organism) { 575 | ph := m.api.GetPhAtPoint(o.Location) 576 | ideal := o.Traits().IdealPh 577 | tolerance := o.Traits().PhTolerance 578 | if math.Abs(ideal-ph) < tolerance { 579 | m.applyHealthChange(o, c.HealthChangeFromChemosynthesis()*o.Size) 580 | } 581 | } 582 | 583 | func (m *OrganismManager) applyHealthChange(o *organism.Organism, amount float64) { 584 | prevSize := o.Size 585 | o.ApplyHealthChange(amount) 586 | if o.Size > prevSize { 587 | m.addUpdatedPoint(o.Location) 588 | // Organism growth affects ph 589 | m.applyOrganismPhGrowthEffect(o) 590 | } 591 | } 592 | 593 | func (m *OrganismManager) applyAttack(o *organism.Organism) { 594 | m.addUpdatedPoint(o.Location) 595 | m.applyHealthChange(o, c.HealthChangeFromAttacking()*o.Size) 596 | } 597 | 598 | func (m *OrganismManager) calculateAttackEffect(o *organism.Organism) float64 { 599 | return c.HealthChangeInflictedByAttack() * o.Size 600 | } 601 | 602 | func (m *OrganismManager) removeIfDead(o *organism.Organism) bool { 603 | if o.Health > 0.0 { 604 | return false 605 | } 606 | 607 | m.gridMutex.Lock() 608 | m.organismMutex.Lock() 609 | 610 | m.organismIDGrid[o.Location.X][o.Location.Y] = -1 611 | delete(m.organisms, o.ID) 612 | 613 | m.gridMutex.Unlock() 614 | m.organismMutex.Unlock() 615 | 616 | m.api.AddFoodAtPoint(o.Location, int(o.Size)) 617 | m.addUpdatedPoint(o.Location) 618 | 619 | return true 620 | } 621 | 622 | func (m *OrganismManager) applySpawn(o *organism.Organism) { 623 | if success := m.SpawnChildOrganism(o); success { 624 | m.applyHealthChange(o, o.HealthCostToReproduce()) 625 | o.Children++ 626 | } 627 | } 628 | 629 | func (m *OrganismManager) applyFeed(o *organism.Organism) { 630 | m.applyHealthChange(o, c.HealthChangeFromFeeding()*o.Size) 631 | } 632 | 633 | func (m *OrganismManager) calculateFeedEffect(o *organism.Organism) float64 { 634 | return c.HealthChangeFromFeeding() * o.Size 635 | } 636 | 637 | func (m *OrganismManager) applyEat(o *organism.Organism) { 638 | m.applyHealthChange(o, c.HealthChangeFromEatingAttempt()*o.Size) 639 | target := o.Location.Add(o.Direction) 640 | 641 | // apply health change but don't delete food until all eat requests have been 642 | // processed. This may result in more total food being consumed by nearby organisms 643 | // than exists at a given point, but this seems preferable right now to denying 644 | // the eat request altogether or coming up with some perfect way to divvy it up. 645 | amountToEat := m.calculateValueToEat(o, target) 646 | m.api.RemoveFoodAtPoint(target, int(math.Ceil(amountToEat))) 647 | m.applyHealthChange(o, amountToEat) 648 | } 649 | 650 | func (m *OrganismManager) calculateValueToEat(o *organism.Organism, target utils.Point) float64 { 651 | if item, found := m.api.GetFoodAtPoint(target); found { 652 | maxCanEat := o.Size 653 | return math.Min(float64(item.Value), maxCanEat) 654 | } 655 | return 0 656 | } 657 | 658 | func (m *OrganismManager) applyMove(o *organism.Organism) { 659 | m.applyHealthChange(o, c.HealthChangeFromMoving()*o.Size) 660 | 661 | targetPoint := o.Location.Add(o.Direction) 662 | if m.isMatchingPositionRequest(targetPoint, o.ID) == false { 663 | return 664 | } 665 | 666 | m.addUpdatedPoint(o.Location) 667 | m.addUpdatedPoint(targetPoint) 668 | 669 | o.TraveledDist++ 670 | 671 | m.gridMutex.Lock() 672 | m.organismIDGrid[o.Location.X][o.Location.Y] = -1 673 | m.organismIDGrid[targetPoint.X][targetPoint.Y] = o.ID 674 | m.gridMutex.Unlock() 675 | 676 | o.Location = targetPoint 677 | } 678 | 679 | func (m *OrganismManager) applyRightTurn(o *organism.Organism) { 680 | m.applyHealthChange(o, c.HealthChangeFromTurning()*o.Size) 681 | 682 | o.Direction = o.Direction.Right() 683 | } 684 | 685 | func (m *OrganismManager) applyLeftTurn(o *organism.Organism) { 686 | m.applyHealthChange(o, c.HealthChangeFromTurning()*o.Size) 687 | 688 | o.Direction = o.Direction.Left() 689 | } 690 | 691 | // GetAllOrganismInfo returns a map of all organisms' Info 692 | func (m *OrganismManager) GetAllOrganismInfo() map[int]*organism.Info { 693 | infoMap := make(map[int]*organism.Info) 694 | m.organismMutex.RLock() 695 | for id, o := range m.organisms { 696 | info := o.Info() 697 | infoMap[id] = info 698 | } 699 | m.organismMutex.RUnlock() 700 | return infoMap 701 | } 702 | 703 | func (m *OrganismManager) printOrganismInfo(o *organism.Organism) string { 704 | return fmt.Sprintf("\n ID: %10d | InitialHealth: %4d"+ 705 | "\n Age: %10d | MinHealthToSpawn: %4d"+ 706 | "\nChildren: %10d | MinCyclesToSpawn: %4d"+ 707 | "\nAncestor: %10d | "+ 708 | "\n Health: %10.2f | ChanceToMutateTree: %4.2f"+ 709 | "\n CalcAndUpdateSize: %10.2f | MaxSize: %4.2f"+ 710 | "\n Tree:\n%s", 711 | o.ID, int(o.InitialHealth()), 712 | o.Age, int(o.MinHealthToSpawn()), 713 | o.Children, o.MinCyclesBetweenSpawns(), 714 | o.OriginalAncestorID, 715 | o.Health, o.ChanceToMutateDecisionTree(), 716 | o.Size, o.MaxSize(), 717 | o.GetDecisionTreeCopy().Print()) 718 | } 719 | --------------------------------------------------------------------------------