Jump to content
  • Advertisement
Sign in to follow this  
  • entries
    25
  • comments
    26
  • views
    26488

About this blog

Web-based snail simulation

Entries in this blog

 

Running tests and getting coverage reports on server deployment in SnailLife

Yesterday I added tests to server deployment in my deployServer.sh script: 
  echo "Running tests" cd ../../server/lib set -e echo "mode: set" > allcoverage.out for d in $(go list ./... | grep -v vendor); do     parentdir=`dirname "$d"`     subdir=`basename "$d"`     echo "subdir: " $subdir     if [[ $subdir == "tests" ]]; then         go test -cover -coverpkg=$parentdir -coverprofile=profile.out $d     else         go test -cover -coverprofile=profile.out $d     fi     if [ -f profile.out ]; then         tail -n+2 profile.out >> allcoverage.out         rm profile.out     fi done
Basically this goes into the root where all of my server packages live. Then for each found package we get the subdirectory name and the full path of the parent directory. If the subdirectory is named "tests" (most of my tests are in packages under the package I'm actually testing), we run go test -cover with -coverpkg specified as the parent dir of the test dir. Otherwise we do not specify -coverpkg because it is in the same directory as the test.  At the end we get an allcoverage.out file which can be opened in the browser to view coverage for each tested source file:

Liza Shulyayeva

Liza Shulyayeva

 

Server and client for SnailLife Go

Over the last couple of days I restructured SnailLife Go into a server and client. I’m still in the “rough draft” stage, but the top level project structure now looks like this: gosnaillife ├── client ├── cmd ├── common ├── LICENSE.md ├── README.md ├── server └── setup Intent Split application into server and client CLI apps. Create REST API for client-server communication Have a “common” package for structures which will be reused by both server and client Create some rudimentary deployment scripts for both server and client main.go I started by creating snaillifesrv/main.go alongside snaillifecli/main.go The cmd directory now looks like this: cmd ├── snaillifecli │   └── main.go └── snaillifesrv └── main.go snaillifecli/main.go The client main.go runs some simple configuration with viper (right now there is just a server.json config file with the server url to connect to depending on which environment you are running). After running the configuration it waits for user input. Once input is received, it tries to find and run a cobra command by that name. package main import "fmt" import ( "os" "bufio" "gitlab.com/drakonka/gosnaillife/client/lib/interfaces/cli" "gitlab.com/drakonka/gosnaillife/client/lib/interfaces/cli/commands" "runtime" "strings" "path/filepath" "io/ioutil" "github.com/spf13/viper" "gitlab.com/drakonka/gosnaillife/common/util" ) func main() { fmt.Println("Welcome to SnailLife! The world is your oyster.") configureClient() if err := commands.RootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } waitForInput() } func configureClient() { projectRoot := getProjectRootPath() confPath := projectRoot + "/config" envname, err := ioutil.ReadFile(confPath + "/env.conf") if err != nil { util.HandleErr(err, "") } envFile := string(envname) configPath := confPath + "/" + envFile viper.AddConfigPath(configPath) // Config client viper.SetConfigName("server") err = viper.ReadInConfig() if err != nil { util.HandleErr(err, "") } } func waitForInput() { buf := bufio.NewReader(os.Stdin) fmt.Print("> ") input, err := buf.ReadBytes('\n') if err != nil { fmt.Println(err) } else { cmd, err := cli.TryGetCmd(string(input)) if err != nil { fmt.Println(err) } else { err := cmd.Execute() if err != nil { fmt.Println("ERROR: " + err.Error()) } } } waitForInput() } func getProjectRootPath() string { _, b, _, _ := runtime.Caller(0) folders := strings.Split(b, "/") folders = folders[:len(folders)-2] path := strings.Join(folders, "/") basepath := filepath.Dir(path) + "/client" return basepath } snaillifesrv/main.go When launching snaillifesrv, a subcommand is expected immediately. Right now the only supported subcommand is serve, which will start the server. package main import "fmt" import ( "gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/env" "os" "runtime" "path/filepath" "strings" "gitlab.com/drakonka/gosnaillife/server/lib/infrastructure" "gitlab.com/drakonka/gosnaillife/common" "gitlab.com/drakonka/gosnaillife/server/lib/interfaces/cli/commands" ) var App env.Application func main() { setProjectRootPath() confPath := env.ProjectRoot + "/config" App = infrastructure.Init(confPath, common.CLI) if err := commands.RootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } func setProjectRootPath() { _, b, _, _ := runtime.Caller(0) folders := strings.Split(b, "/") folders = folders[:len(folders)-2] path := strings.Join(folders, "/") basepath := filepath.Dir(path) + "/server" env.ProjectRoot = basepath } Client So far an extremely barebones implementation, it looks like this: client ├── config │   ├── config.go │   ├── dev │   │   └── server.json │   └── env.conf └── lib └── interfaces └── cli ├── cli.go ├── cmd.go └── commands ├── register.go ├── root.go └── test.go Right now only the register command is implemented.  Server The server is where the bulk of the existing packages ended up going: server ├── config │   ├── config.go │   ├── dev │   │   ├── auth.json │   │   └── database.json │   └── env.conf └── lib ├── domain │   ├── item │   └── snail │   ├── snail.go │   └── snailrepo.go ├── infrastructure │   ├── auth │   │   ├── authenticator.go │   │   ├── auth.go │   │   ├── cli │   │   │   ├── auth0 │   │   │   │   ├── auth0.go │   │   │   │   └── tests │   │   │   │   ├── auth0_test.go │   │   │   │   └── config_test.go │   │   │   ├── cli.go │   │   │   └── cli.so │   │   ├── provider.go │   │   └── web │   ├── databases │   │   ├── database.go │   │   ├── mysql │   │   │   ├── delete.go │   │   │   ├── insert.go │   │   │   ├── mysql.go │   │   │   ├── retrieve.go │   │   │   ├── tests │   │   │   │   └── mysql_test.go │   │   │   └── update.go │   │   ├── repo │   │   │   ├── repo.go │   │   │   ├── tests │   │   │   │   ├── repo_test.go │   │   │   │   ├── testmodel_test.go │   │   │   │   └── testrepo_test.go │   │   │   └── util.go │   │   └── tests │   │   └── testutil.go │   ├── env │   │   └── env.go │   ├── init.go │   └── init_test.go └── interfaces ├── cli │   └── commands │   ├── root.go │   └── serve.go └── restapi ├── err.go ├── handlers │   └── user.go ├── handlers.go ├── logger.go ├── restapi.go ├── router.go └── routes.go I followed a lot of the advice from this useful post about creating REST APIs in Go. When the user runs the register command on the client, here is what happens on the server. I have added comments to the copy below to help explain: package handlers import ( "encoding/json" "fmt" "errors" "io/ioutil" "io" "net/http" "gitlab.com/drakonka/gosnaillife/common/restapi" "gitlab.com/drakonka/gosnaillife/common/util" "strings" "gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/auth" "gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/env" http2 "gitlab.com/drakonka/gosnaillife/common/util/http" ) func CreateUser(w http.ResponseWriter, r *http.Request) { fmt.Println("Creating user") var user restapi.UserReq body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) // The infamous Go error handling - I need a better way. if err != nil { util.HandleErr(err, "CreateUserErr") return } if err := r.Body.Close(); err != nil { util.HandleErr(err, "CreateUserErr") return } // Unmarshal the data we get from the client into UserReq if err := json.Unmarshal(body, &user); err != nil { // If we were unable to unmarshal, send an error response back to the client util.HandleErr(err, "CreateUserErr") w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(422) // unprocessable entity if err := json.NewEncoder(w).Encode(err); err != nil { util.HandleErr(err, "CreateUser") return } return } fmt.Println("Running registration") resBody, err := registerUser(user) if err != nil { util.HandleErr(err, "CreateUserErr") } // Start creating a userRes to send back to the client. userRes := buildUserResponse(resBody) status := http.StatusOK if err != nil { status = http.StatusInternalServerError } w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(userRes); err != nil { util.HandleErr(err, "CreateUserErr") return } } func registerUser(user restapi.UserReq) (resBody []byte, err error) { // Find an Auth0 provider (that is all we'll support for now) var auth0 auth.Provider auth0 = env.App.Authenticator.FindProvider("Auth0") if auth0 != nil { resBody, err = auth0.Register(user.Username, user.Password) } else { err = errors.New("Auth0 provider not found") } return resBody, err } func buildUserResponse(resBody []byte) (*restapi.UserRes) { res := restapi.UserRes{} // Find any keys we may find relevant from the Auth0 response body m, _ := util.FindInJson(resBody, []string {"_id", "statusCode", "name", "description", "error"}) httpErr := buildHttpErr(m) if id, ok := m["_id"]; ok { res.Id = fmt.Sprintf("%v", id) } res.HttpErr = httpErr return &res } func buildHttpErr(m map[string]interface{}) (httpErr http2.HttpErr) { // The Auth0 response body *sometimes* contains errors in statusCode/name/description format and *sometimes* just contains a single "error" json key if sc, ok := m["statusCode"]; ok { codeStr := fmt.Sprintf("%v", sc) if strings.HasPrefix(codeStr,"4") || strings.HasPrefix(codeStr, "5") { scf := sc.(float64) httpErr.StatusCode = int(scf) httpErr.Name = fmt.Sprintf("%v", m["name"]) httpErr.Desc = fmt.Sprintf("%v", m["description"]) } } else if error, ok := m["error"]; ok { httpErr.StatusCode = 500 httpErr.Name = "Error" httpErr.Desc = fmt.Sprintf("%v", error) } return httpErr } In the end the server sends a UserRes back to the client package restapi import ( "gitlab.com/drakonka/gosnaillife/common/util/http" ) type UserRes struct { HttpErr http.HttpErr `json:"httpErr"` Id string `json:"id"` Username string `json:"username"` } type UserReq struct { Username string `json:"username"` Password string `json:"password"` Connection string `json:"connection"` } Deployment I made a couple of quick scripts to deploy client and server. Note that go-bindata lets you compile your config files into the binary, making for easier distribution (and maybe slightlyimproved security for the secret keys stored in the server config since you don’t have loose configs with credentials sitting around) Client #!/bin/sh echo "Building and installing SnailLife" go-bindata -o ../../client/config/config.go ../../client/config/... cd ../../cmd/snaillifecli; go build GOBIN=$GOPATH/bin go install Server #!/bin/sh echo "Building and installing SnailLife server" go-bindata -o ../../server/config/config.go ../../server/config/... cd ../../server/lib/infrastructure/auth/cli echo "Building cli.so auth plugin" go build -buildmode=plugin -o cli.so echo "Building SnailLifeSrv" cd ../../../../../cmd/snaillifesrv; go build GOBIN=$GOPATH/bin go install Anyway, as you can see there is a long way to go. Up next I am going to write some tests for the REST API and the cobra commands (which I should really have been doing already).

Liza Shulyayeva

Liza Shulyayeva

 

Go auth (and other) progress

July 28 Made a bit more progress on the authentication basics today. Relevant commits are: Add http package; have auth0 test delete user it has just registered after test is done Create json util; add logout test July 29 Today I focused a bit on the building and installation of snaillifecli. I switched my custom app configuration code for Viper because it apparently integrates really well with Cobra, which is a library to help make CLI applications. It is really tempting to avoid plugging in existing libraries and write everything from scratch because I’m positive that it will teach me a lot about Go, but the existing solutions seem more than suitable and I want to get to working on actual snails at some point. I also checked in a couple of quick scripts to build and install the app. deployDebug deploys to a subdirectory under GOBIN and copies the config file the app will use alongside the executable. This is really dangerous because it means database credentials are exposed to whoever wants to look in the config file and is to be used for local debug purposes only. The deployProd script first runs go-bindata to generate a go file from the json config and have the configuration compiled into the binary during the build step. This way any sensitive database credentials and such are not directly exposed. Of course though, I don't plan on distributing any binary with secret key information in it to external users.

Liza Shulyayeva

Liza Shulyayeva

 

Playing with authentication in Go

It’s almost 2am and I’m sleepy, but I wanted to write this down while it’s relatively fresh in my mind. I’ve been playing around with user authentication in Go. While actual user interaction is not the primary part of the simulation and will not be the focus in the Go rewrite as I said in my previous post, there will need to be a few basic actions that a user will take before leaving the rest of the simulation to do its thing. This is why I mentioned implementing a very basic CLI to interact with the simulation earlier. The user will basically just need to sign up, log in, set some basic options for their snail stable (like the stable name and location), capture a snail or two, and leave them to do their thing from there. It is kind of like norn Wolfling runs in Creatures 3 - you have to hatch some norns before you let nature take its course. Design differences from the PHP version The main difference from the way the concept of users is currently implemented in the PHP version of SnailLife is this: users will no longer be considered synonymous with owners. In the PHP version of SnailLife, users and owners are just one table - account details, moderator/BrainHub management access fields, and stable information are all stored in one location. In my opinion this isn’t the best approach. A user of the snail simulation will not neccessarily need to be an owner of a stable or of any snails. This is especially true considering I’m building this thing with multiple possible applications in mind. Approach I haven’t worked much with authentication systems before - the PHP version of the app made use of the authentication features that came with Laravel. So this is largely going to be a matter of trial and error. The commit with the rough first stage of this can be found here. Here are the highlights: For the Go rewrite I am thinking of using Auth0. I suspect I may need different auth approaches for web and cli authentication, and my first thought is to separate these into plugins. I have added a client type enum to the Application struct that is populated on app init. If the application is of type CLI, the CLI authentication plugin will be loaded. If it is of type Web, the web authentication plugin will be loaded (but I am only implementing the CLI version for now). The CLI plugin is built like this: go build -buildmode=plugin -o cli.so, and the authorizer then imports cli.so (or web.so, which is currently not built) The plugin could potentially have multiple possible providers, but right now I am only implementing Auth0 - each provider is to implement the Provider interface defined outside of the plugin in the auth package. I have added an auth.json to the conf directory (in gitignore of course), and also added a separate credentials generator for auth0 testing (also gitignored). So far I have a registration and login test implemented - the test generates a random username (email) and password each run to test registration and login. Authentication package structure is currently as follows: auth ├── authenticator.go ├── auth.go ├── cli │   ├── auth0 │   │   ├── auth0.go │   │   └── tests │   │   ├── auth0_test.go │   │   └── config_test.go │   ├── cli.go │   └── cli.so ├── provider.go └── web    

Liza Shulyayeva

Liza Shulyayeva

 

Trying out Go

A couple of weeks ago I had the genius idea to rewrite SnailLife in Go. I’ve already looked into doing this once before a couple of years ago, but wasn’t really feeling it and stuck with PHP (mostly for nostaligia reasons). Now though, SnailLife is this bloated PHP app. Most of the core functionality is in. After weeks of battling infrastructure issues, when everything was back up and running again, I took a step back and saw how big the app (or rather the 3 apps now) had become. At the same time I’d been reading in passing about Go and became curious, so I figured - why not look into learning Go by rewriting SnailLife? Not because I think Go itself will necessarily make anything better, but because a rewrite might. The features are mostly already designed, reimplementing them in another language would hopefully let me focus more on improving the overall project structure while learning the new language of choice. Of course, the “learning the new language of choice” part also increases the likelihood of my turning my messy PHP app into a messy Go app as I go, but…it’ll be fun, OK? Anyway, I’m not yet sure if I’ll stick with the Go port or if I’m just amusing myself for a while before going back to the already largely implemented PHP version. So far I haven’t coded anything snail-specific and have instead been focusing on setting up database-related packages. I’ve made the code public on GitLab for now, though not sure if that’ll change when I go into writing the more snail-specific functionality: https://gitlab.com/drakonka/gosnaillife When I started the PHP version of SnailLife, I started by building the website and the main functionality that lets users interact with their snails. As time went on this focus switched almost exclusively to the back-end, and to working on functionality that required no user interaction. I realized that this is what the core of the idea was - simulating the actual snails - the brain, organ function, etc - things that the user could eventually influence indirectly, but things that would tick away on their own even if no user was involved. So for the Go version I am not starting with a web front end but with a simple CLI, and focusing on implementing the core of the snail itself first. Eventually I can build whatever front-end I want, or even multiple front-ends if I feel like it. Heck, I could even expose some sort of API for others to make their own apps on top of the simulation (if anyone wanted to, in theory). Go notes to self Open and close DB connections as little as possible - the driver handles connection pooling for you, you should only really need to do it once. Best way of reusing constructors between tests might be to create some test utilities outside of _test files which are imported only by the tests. Example usage in my case is creating a test db and table to run my mysql and repo tests against, which are in different packages. Every directory is a package. There is no way to structure code in subdirectories without each subdirectory being a separate package. Make use of table driven tests. They allow you to run multiple test cases per test. interface{} is an empty interface and can hold values of any type…avoid passing this around too much, better to learn to structure the code so you don’t have to. Go code looks to be very easy to move around and restructure if needed, so it should be fine to experiment with different project structures as I go. Current tentative project structure drakonka/gosnaillife ├── cmd │ └── snaillifecli │ └── main.go ├── config │ ├── dev │ │ └── database.json │ └── env.conf ├── lib │ ├── domain │ │ ├── item │ │ └── snail │ │ ├── snail.go │ │ └── snailrepo.go │ ├── infrastructure │ │ ├── databases │ │ │ ├── database.go │ │ │ ├── mysql │ │ │ │ ├── delete.go │ │ │ │ ├── insert.go │ │ │ │ ├── mysql.go │ │ │ │ ├── retrieve.go │ │ │ │ ├── tests │ │ │ │ │ └── mysql_test.go │ │ │ │ └── update.go │ │ │ ├── repo │ │ │ │ ├── repo.go │ │ │ │ ├── repoutil.go │ │ │ │ └── tests │ │ │ │ ├── repo_test.go │ │ │ │ ├── testmodel_test.go │ │ │ │ └── testrepo_test.go │ │ │ └── tests │ │ │ └── testutil.go │ │ ├── env │ │ │ └── env.go │ │ ├── init.go │ │ ├── init_test.go │ │ └── util │ │ ├── collection.go │ │ └── err.go │ ├── interfaces │ └── usecases  

Liza Shulyayeva

Liza Shulyayeva

 

State of the Snail - Debugging Hell

It has been a while! SnailLife work has been moving at a snail's pace. Why? Because debugging the snail brain has turned into a highly demotivating, hellish endeavour. The snails make decisions and perform actions based on so much *crap*, that the simple log files I was using all long are just not cutting it anymore. The Laravel log for the original app has turned into 3 Laravel logs because I now have 3 independent apps (the front-end, BrainRunner, and BrainHub to coordinate all the brain runners). That then turned into individual log files per snail plus individual log files per brain task. And still making sense of why snails are choosing to behave in certain ways is nigh impossible - the brain *and* infrastructure issues I have been facing have been seemingly neverending. Logging issues aside I have added some debug settings to help me narrow down issues. The BrainRunner app now has a snail debug config file which allows me to disable the following actions that would otherwise take place on each brain check:
growth
life check
idle action check
organ impact check
bodyweight check
movement
feelings of claustrophobia
feelings of loneliness
More settings will be added, such as disabling sensory/short term/long term memories, for example. The "idle action check" is where the bulk of the "magic" happens, that is the biggest black hole. Adding options to disable certain functionality has helped, but the whole thing has been demotivating to say the least. I got into this to simulate snails, not work on debugging tools. And yes, of course I realize when dealing with a convoluted system like this I should have known what to expect. I did sort of expect this...I just chose to ignore it until the last minute to work on more fun things ;) After putting in a great deal of effort to remain organized and do things right in my projects at work, I've allowed good behaviour to fly out the window at home. Anyway, I have now moved logging out of local log files and into Loggly. It's not done yet, but the main logs like Laravel logs from the BrainRunner and BrainHub, snail logs, and brain task logs are all in Loggly now (in addition to the local machine). To send the default Laravel logs to Loggly for each app I added the `configureMonologUsing` method in bootstrap/app.php:
$app->configureMonologUsing(function ($monolog) use ($app){ $today = date("Y-m-d"); $logFileName = "logs/laravel-$today.log"; $monolog->pushHandler(new MonoStreamHandler(storage_path($logFileName), MonoLogger::INFO)); $monolog->pushHandler(new MonoStreamHandler(storage_path($logFileName), MonoLogger::WARNING)); $monolog->pushHandler(new MonoStreamHandler(storage_path($logFileName), MonoLogger::ERROR)); $monolog->pushHandler(new MonoStreamHandler(storage_path($logFileName), MonoLogger::CRITICAL)); if (CoreUtility::InternetOn()) { $envlabel = strtolower(env('APP_ENV')); $maintag = "brainrunner_$envlabel"; $envtag = "env_$envlabel"; $logglyString = CoreUtility::buildLogglyPushHandler(array($maintag, $envtag)); $monolog->pushHandler(new LogglyHandler($logglyString, MonoLogger::INFO)); $monolog->pushHandler(new LogglyHandler($logglyString, MonoLogger::WARNING)); $monolog->pushHandler(new LogglyHandler($logglyString, MonoLogger::ERROR)); $monolog->pushHandler(new LogglyHandler($logglyString, MonoLogger::CRITICAL)); } return $app;}); Aside from having all of my logs in a central place, Loggly lets me query the logs easily to get more relevant information. I use tags to distinguish between individual brain tasks, snails, apps, environments, etc. But that is not enough. I feel to debug the brain in its current state having a visual representation of the brain would help a great deal. What originally made me think about this was my earlier experimentation with TensorFlow and TensorBoard. TensorBoard provides a visual of your TensorFlow graph and lets you interact with said visual to see how tensors flow between operations. The brain as I have it can also be represented as a set of nodes (each node representing a neuron), and I should be able to visualize the flow of inputs between neurons in a similar way. What if I could have a graph for every brain task that runs on every BrainRunner and see exactly what path each input took and why? I think I will look into this next. As you can see I have not been doing much work on the actual *snails* here. But I think the return on investment in improved debugging tools will be worth it if I have any hope of getting any further with simulating a snail brain. On another note it looks like ECAL 2017 ticket prices are out and I'd better start saving: https://project.inria.fr/ecal2017/registration/

Liza Shulyayeva

Liza Shulyayeva

 

Snaily Updates: BrainHub + Runners, SnailLife Logo

I finally have two BrainRunners working on a DigitalOcean droplet, and one BrainHub on another droplet queueing and assigning tasks to the runners.

It's still rough, but let's start with the BrainHub's scheduled artisan commands (Artisan is the CLI that comes with Laravel):

[font='courier new']QueueIdleBrainChecks[/font] runs every minute.
public function handle() { // Find snail due for brain check. $snailController = new SnailController(); $allIdleSnails = $snailController->getAllIdleSnails(); foreach ($allIdleSnails as $snail) { // Seconds since last brain check $diff = Carbon::now()->diffInSeconds (Carbon::parse($snail->brainCheckedAt)); if ($diff >= 60) { $existingQueuedBrainCheck = QueuedBrain::where('snailID', '=', $snail->snailID)->first(); // If brain check is not already queued, queue a new check if ($existingQueuedBrainCheck === null) { $queuedBrain = new QueuedBrain(); $queuedBrain->snailID = $snail->snailID; $queuedBrain->save(); } } } }
This basically just gets all living idle snails from the `snail_life` db and creates a new brain check entry in the `brain_hub` db.

Also every minute we run the [font='courier new']AssignBrainsToRunners[/font] artisan command:
public function handle() { $brainRunnerRetriever = new BrainRunnerRetriever(); $allIdleBrainRunners = $brainRunnerRetriever->getAllIdleBrainRunners(); $taskRetriever = new TaskRetriever(); foreach ($allIdleBrainRunners as $brainRunner) { $task = $taskRetriever->GetNextQueuedTask(); if ($task !== null) { $brainRunner->assignQueuedTask ($task); } } }

This finds any available (idle) brain runners and assigns the next queued tasks to them.

In the `BrainRunner` model:
public function assignQueuedTask($task) { // Change status of BrainRunner to 1 - Busy $this->updateStatusCode(1); $url = $this->url() . DIRECTORY_SEPARATOR . 'api/runTask'; Log::info('assignQueuedTask url: ' . $url); $postfields = 'taskID=' . $task->id . '&snailID=' . $task->snailID . '&runnerID=' . $this->id . '&hubURL=' . env('APP_URL'); $curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => $url, CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $postfields, CURLOPT_TIMEOUT_MS => 2500 )); $resp = curl_exec($curl); curl_close($curl); // Delete task from queue $task->delete(); }

The brain runner then runs the task:

public function RunTask($task) { try{ $this->taskID = $task['taskID']; $this->hubURL = $task['hubURL']; $this->runnerID = $task['runnerID']; // Get runner specific logger $utility = new Utility($this->taskID); $logger = $utility->logger; $snailController = new SnailController(); $logger->addInfo('INFO: RunTask: Initialized SnailActionController'); // The recurring event is actually not required anymore, // but we kind of hack it together because before the BrainHub // the snail brains relied on it and still do temporarily. $event = new RecurringEvent(); $event->name = 'IdleSnailAction'; $logger->addInfo('INFO: RunTask: Initialized IdleSnailAction Event'); // Find the snail $snail = $snailController->findSnail($task['snailID']); if ($snail === null) { $logger->addError('ERROR: Snail ID ' . $task['snailID'] . ' NOT FOUND.'); } else { $logger->addInfo ('INFO: RunTask: Found Snail ID: ' . $snail->snailID); $snailActionCoordinator = new SnailActionCoordinator(); $snailActionCoordinator->checkAction([$snail], 60, $event); $logger->addInfo ('INFO: RunTask: Action Checked'); } $logger->addInfo ('INFO: RunTask: Reported task as finished'); // Save log to S3 (might rip this out later as we don't need to keep that many logs anyway) $utility->saveLog($this->runnerID); } catch(\Exception $e){ $logger->addError('ERROR: RunTask: Exception Caught, cancelling task'); $logger->addError($e); } $this->reportTaskFinish(); } The BrainHub connects to both the main SnailLife database and the BrainHub database. BrainRunners can only connect to the SnailLife database. The only thing that ever reads from or modifies the BrainHub DB is the BrainHub itself.

SnailLife logo and website

I have been getting really sick of looking at the ugly black and white SnailLife website. So I decided to try and make it a little more exciting. It's still ugly, but at least it's colorful-ugly now! I stumbled across a bunch of open source logos over at Logodust. I felt like taking a break from the BrainHub for a while and messed around with combining two of the logos and adding an eye stalk to make something vaguely resembling a snail. The logo is also quite versatile in case I ever decide to ditch the snail idea, since I've been told it looks like a chinchilla or one of those deep water fish with the light in front of its face as well...
[font='Noto Serif']
[/font][font='Noto Serif']
So now the site is, though by no means great, just a little less bland:[/font][font='Noto Serif']
[/font]

Liza Shulyayeva

Liza Shulyayeva

 

BrainHub and BrainRunner - Finally Over the Hump

Note: This post may make more sense if you also check out the one I posted before Christmas on my blog (which I unfortunately forgot to also post here at the time: http://liza.io/the-brain-scope-is-growing-brainhub/

I've started this post maybe twenty times now, since before Christmas, and each time I keep putting it off, thinking "I'll just blog when I have this next bit done." But each "next bit" is followed by something else, and then something else, into infinity. So I'll just write an update.
Since before the holidays I've been working on BrainHub and BrainRunner, which I've already written about. Basically - checking each brain every minute as part of the main SnailLife app was becoming unmanageable. All background processes will be moved to complementary apps away from the user-facing SnailLife application. The idea is to have a BrainHub controlling tasks sent to individual BrainRunners (which are hosted on other DigitalOcean or EC2 droplets). So, here is the first iteration of the BrainHub admin site. The BrainHub has its own database of queued tasks and BrainRunners but also connects to the main SnailLife database and imports a package called SnailLifeCore to be able to get information about brains due for a check and allow admin SnailLife users to log in/control the hub.
It is sort of functional in that BrainHub runs 2 scheduled tasks:
QueueIdleBrainChecks
AssignBrainsToBrainRunners

Both of these run every minute. The first gets a list of brains that need to be processed and puts them into the queued_brains table in the brainhub db.
The second, AssignBrainsTobrainRunners, looks for any idle brain runners (brain runners with status code 0) and assigns the next brain in the queue to them. Then, the brain runner checks the brain and reports back to BrainHub with the result, which releases the runner to process the next brain in the queue.
Right now there are some issues - runners don't get consistently released, for example. Should be easy enough to fix, but for now I've added an emergency release button to the admin site (you can see it above).
But right right now I am working on logging. The brain runner creates a new log for each brain task it runs. These then need to be backed up to AWS S3 (as opposed to being stored on the Droplet itself), and then the admin site will display the logs by-task for each runner.
There is a mountain of work to do on this but it feels like I'm sort of over the main hump of setting up the core package and the BrainHub and BrainRunner apps to sit alongside the main SnailLife app.

Liza Shulyayeva

Liza Shulyayeva

 

Snail Organs, Immune System, and Ageing

Organs! Immune systems! Old age! Dying from old age!

I'm super excited. I was thinking: "How do I implement old age and death, and health in general? Is health just energy? When the snail is out of energy, it dies? But that seems too simplistic."

At this point I should have realized that 'simplistic' is a good thing. Instead I decided to try to mimic a scenario that is a little closer to real life. Again, I am no snail expert or biologist and mostly I just...guessed. I want to tweak the system when I move more into the research phase of the project vs the implementing random features and fixing bugs phase. I have a feeling this should have been done the other way around.

Anyway - why does any living organism die? It seems that we die either because of some trauma or injury to our internal organs or...old age. But what is old age, really? How is it different from trauma or injury to our internal organs?

From the very brief amount of reading I'd done it's partly about telomeres and how efficiently our cells can keep dividing as we get older. Telomeres protect our chromosomes. Each time a cell divides, our telomeres shorten until eventually our cells can't divide anymore. And then what?

I think that's when we become more prone to age-related diseases. The immune system is apparently highly related to the length of the telomeres. And when your immune system gets worse our bodies can no longer handle various illnesses as well as they used to, which eventually results in that first thing - trauma or injury to our internal organs that we can no longer recover from.



My cat Rigel has a damaged bladder. A vet "broke" him and he has been handicapped ever since. The bad news is that he may now need to be on medication for the rest of his life. The good news is that there is a chance, especially since he is so young, that his bladder will regenerate over time. The cells and nerves of his bladder may still kick themselves into shape over months or years and he may be able to take a little less medicine or maybe, if a miracle happens, eventually none at all!

This is how I want the snails to work. Organ damage can be sustained, but with a high enough immune system the snail may be able to recover and at least partially heal/regenerate itself. As the immune system weakens organ damage becomes permanent and the snail is not as able to recover. Eventually the snail is just too weak, its organs start failing, and it dies.

The details

I've kind of done it in a really hacky way...like all the experimental features so far...which is pretty much all the features.

Organs

So a snail has a base organ efficiency score and an organ efficiency percentage. Eg:
baseHeartEffic: 60
heartEfficPerc: 100%
baseLungEffic: 45
lungEfficPerc: 90%

The organs so far are:




Yes, I know a snail has more organs than this. But I have sort of stayed away from organs that already are represented by sensors in the brain. These include eyes (vision is accounted for), mouth (taste is accounted for), tentacles (touch is accounted for). Theywill be added eventually, but I wanted to focus on the others for now.

All snails start with 100% efficiency for each organ. At first I was thinking of not havingbaseXEffic at all and just have a 0-100 percentage for each snail, but I realized this would be boring. All snails are not created equal. Just as Secretariat had a very large heart, a snail can have a much stronger or weaker organ than average. A simple percentage standard across all snails would not allow for the amazing outliers of nature that we see in real life.



Ageing

Snails already have a maturity rate (the rate at which they grow after birth). I decided to reuse this for ageing. Snails age every hour. Until a snail is mature it grows each hour based on its maturity rate. Once a snail is mature it begins to age and deteriorate every hour. The amounts may be small - something like 0.05% decrease in immunity per hour. You will be able to boost immunity with supplements, medicines, etc, but for now we are just focusing on the snail's natural rate of ageing here.

Each minute we check for idle snail actions. Each minute we will also check for any organ damage as a result of those actions. For example, if a snail is operating at 95-100% physical effort it may have a higher chance of heart, lung, or foot damage.


Recovery

I have not built recovery in yet. I want to see how long a snail might live without any recovery of the organs. Recovery is to follow!

First I think a big refactoring is in order. As I look back at some of the older code I can see a lot of cleanup that needs to be done in general. I think I'll do that over this weekend and then continue.

Liza Shulyayeva

Liza Shulyayeva

 

Wild snails around the world

Did you know that there is an entirely separate formula to calculate the distance between two points on the surface of a sphere? I mean...it kind of makes sense when you think about it. The shortest distance between two points without accounting for the curvature of the sphere would go right through the sphere itself, making it pretty useless when calculating physical distance on, for example, the surface of the Earth.

I've been working on creating some variation in the attributes of wild snails you might catch in different parts of the world. It's not perfect, but the snails you find in Mobile, Alabama now look generally different from the snails you might find in Stockholm, Sweden.
So far the general rules are thus:
The snail shells get less red and more blue in them as you travel away from the equator
Snail shells get more green in them as you travel East
Snail pattern colors are the opposite of the above
Snail eye colors are always randomized
Snail pattern shapes and sizes also remin random for now, but will definitely change with the region at some later stage

I have added an idealTemp column to the snail table. The ideal temperature of snails you find in the wild in various regions tends to change (warmer toward equator and colder toward the poles). This means you will need to be quite careful when trading with others or buying other people's snails (or even sending your snail overseas for a race on a foreign track). You'd need to closely control temperature and evaluate how your snail might perform on a track where it is currently 40C when it is used to a temperature of 20C. Temperature conditioning will need to be a thing.

Admittedly with these rules the snails you tend to find end up being a little more boring. Instead of a totally random mishmash of colors you end up with something more constrained. But I think this is ok. Wild snails are not meant to be beautiful or exciting - users are supposed to breed for those traits. If you regularly find unique, amazing snails in the wild there won't be as much incentive or excitement in learning about your snail's genetic traits and breeding for a desired result.

As I mentioned before, it takes a couple of seconds for HTML5 geolocation to get your coordinates (and then a few more seconds to get city/country name via the Geoname API). If you for some reason click to search for a wild snail before your location is loaded the coordinates of the search default to the coordinates of your stable.

In addition users will eventually be able to release their snails into the wild. At first it will just mean that someone else can find them. Down the line maybe the wild snails will be able to breed amongst themselves and change the attributes of wild snails in that region.
I have taken some screenshots of wild snails you may find in different places. Here they are:

Mobile, Alabama, US


Setauket, New York, US


San Francisco, California, US


Perth, Western Australia


Stockholm, Sweden


Kyoto, Japan


The North Pole

Liza Shulyayeva

Liza Shulyayeva

 

Bringing snail stables down to earth

SnailLife snail stables have always lived in the ether - in some virtual universe with no physical location. Since I'm trying to make SnailLife based in reality, users' snail stables should also be based in real locations. This is why now, upon registration, the user's physical location is used as the location of their snail stable.

Having never used HTML5 geolocation features before, I kind of winged it and hacked together something that works for now. Currently I get the user's location in two cases - on registration, and when searching for wild snails.

Here is how it happens on the registration page:

Problem - retrieving the location takes a few seconds. If the user happens to register too quickly their location will not be retrieved. As a potential solution I am considering disabling the Submit button until the location is ready...however, what if they don't want to allow me to retrieve their location at all?

If the user does not let me retrieve their location, it will be set by default to some place in Louisiana. Maybe then I can just display a "Don't want to be in Louisiana? Wait while we get your location!" message...

So the latitude and longitude is stored for the user's new account. Then to get the country and city name when displaying where the user is we use the Geonames API. Example:
public static function GetCityName($latitude, $longitude) { $cityName = null; if (Utility::InternetOn()) { $url = "http://api.geonames.org/findNearbyPlaceName?lat=" . $latitude . "&lng=" . $longitude . "&username=myusername"; Log::info ('connection'); $xmlDoc = new \DOMDocument(); $xmlDoc->load($url); $cityNameNode = $xmlDoc->getElementsByTagName("name"); $cityName = null; if ($cityNameNode->length > 0) { $cityName = $cityNameNode->item(0)->nodeValue; } } return $cityName;}
Right now in my snail stable it is 15.5 degrees Celsius. I don't think I'm going to simulate building insulation, so without temperature control it would be about the same temperature in each jar (I may take humidity from the jar's substrate into account, and have other items adding warmth or cold aside from temperature control gadgets). The user won't see their exact jar temperature until they install a thermometer in the jar, and they won't be able to regulate the temperature without installing a temperature controller.

Snail jars have had temperature for a long time, but now that temperature is influenced by real life weather at the stable's location. This will in turn influence the snails within the jar: their health, mood, etc (this last part is not yet implemented...well, it is only implemented very very roughly).

To get temperature I originally tried OpenWeatherMap but that seems very slow and unreliable. So I am trying out The Dark Sky API:
public static function GetCurrentTemperature($latitude, $longitude) { $url = "https://api.forecast.io/forecast/MYAPIKEY/" . $latitude . "," . $longitude; $JSON = file_get_contents($url); $data = json_decode($JSON); $f = $data->currently->temperature; $c = Utility::FahrenheitToCelsius($f); return $c;}
The Dark Sky gives me 1000 free API calls per day. I blew through that in under an hour because I was stupid and calling the API each time I needed to get the temperature. Which is a lot...passive events alone blow through about 200 calls in 5 minutes.

So I added a [font='courier new']currentTemp[/font] field in the User table and a [font='courier new']temp_updated_at[/font] field. I added a recurring event to update the temperature once per hour.
Different snails will adapt to live better in certain climates, and you will find different kinds of snails in different places. This way if you happen to physically be traveling the world you could hunt for wild snails away from your stable and find totally different patterns, colors, and traits.

Aside from affecting snail health and such in the future, temperature in a jar already affects how quickly consumable items rot. Things rot faster at a higher temperature.

Up next I want to think of a good way to vary snail types based on coordinates. There will be some manually set snail types that are available only in certain areas of the world if I want to make some super cool rare location (eg - get a special snail if you're at the NASA headquarters!), but for the most part I want to create that variation automatically. I just have to think of a good way to approach this. I might even embed a Google Map on the wild-snail-collection page so that instead of just clicking "Look under a rock" or whatever you can click on a spot in your general vicinity on a map and see what snail pops up there!

Liza Shulyayeva

Liza Shulyayeva

 

Laravel Log File Backups to S3

SnailLife does a lot of logging for debugging purposes. Aside from the general [font='courier new']laravel.log[/font] I have separate loggers for each snail and also write logs for stuff like deleted items etc in separate files.

The problem is all the space this takes up on my Digital Ocean droplet in Laravel's storage folder. If I leave it for a week or two it fills up and I'll suddenly find my droplet inaccessible or some recurring commands not being able to finish properly due to insufficient space.

Instead of culling the logs more aggressively I decided to set up a backup to Amazon S3. With Laravel 5's filesystems this ended up being a pretty simple process.

First I set up an S3 bucket called [font='courier new']snaillife-storage [/font]with a user that has [font='courier new']getObject[/font], [font='courier new']createObject[/font], and [font='courier new']deleteObject[/font] permissions.

I set the S3 credentials in the [font='courier new'].env[/font] configuration file:
S3_KEY=blahS3_SECRET=blahS3_REGION=website-us-east-1S3_BUCKET=snaillife-storage
Note that I set the region here just in case but in reality I don't use it. In [font='courier new']config/filesystems.php[/font] I set up the S3 disk using these credentials (the region setting is removed. I also changed the local disk root to [font='courier new']storage_path()[/font]):
'local' => [ 'driver' => 'local', 'root' => storage_path(),],'s3' => [ 'driver' => 's3', 'key' => env('S3_KEY'), 'secret' => env('S3_SECRET'), 'bucket' => env('S3_BUCKET'),],
Then I made a new artisan command called BackUpLogs:
allFiles('logs'); $cloudDisk = Storage::disk('s3'); $pathPrefix = 'snailLogs' . DIRECTORY_SEPARATOR . Carbon::now() . DIRECTORY_SEPARATOR; foreach ($localFiles as $file) { $contents = $localDisk->get($file); $cloudLocation = $pathPrefix . $file; $cloudDisk->put($cloudLocation, $contents); $localDisk->delete($file); } } else { Log::info('BackUpLogs not backing up in local env'); } }}
Note that the directory you specify in [font='courier new']$localDisk->allFiles($dir)[/font] should be relative to the root path of the local disk - an absolute path does not work.

In [font='courier new']Kernel.php[/font] I set this to run every hour:
$schedule->command('BackUpLogs')->cron('5 * * * *');
So now every hour all the files in my storage/logs directory are backed up to my S3 bucket and deleted from the local disk.

Liza Shulyayeva

Liza Shulyayeva

 

SnailLife messaging system

"What's SnailLife", you say? Well I'm glad you asked! My snail simulation has gone through a couple of names...and even though I was never 100% happy with Gastropoda it was the best I could come up with - a name that was unique and didn't allow the project to sound too "gamey" (because it's not a game). All of the cooler names I could come up with weren't suitable for various reasons (like domain name availability and such).

But recently I found out about the .life TLD! And I decided that nowadays we have such a varied domain name landscape that .com isn't as important as it used to be, and definitely not for an obscure hobby project. So Gastropoda is now SnailLife!
On to the messaging:

Messaging/Notifications

I noticed when working on the simulation that it was tough to figure out who died and why when snails disappeared from a jar.

(Sidenote...I just realized...in real life a snail wouldn't just disappear from a jar if it died. It would sit there and start decomposing until someone put it away. Maybe this should be the case with SnailLife, too).

Anyway, I wanted something to notify me immediately when something important happens, like a snail is born or dies. So I made a rudimentary messaging system to receive notifications from the simulation, which should be able to be pretty easily expanded into a user-to-user messaging system.

First I made a [font='courier new']user_messages[/font] table with the following columns:
messageID
recipientID
senderID
subject
content
isRead
created_at
updated_at

Then I made a [font='courier new']UserMessage[/font] model that looks like this for now:


'integer', 'senderID' => 'integer', 'subject' => 'alpha_num_spaces', 'content' => 'alpha_num_spaces', 'isRead' => 'boolean' ); protected $primaryKey = 'messageID'; protected $fillable = ['recipientID', 'senderID', 'subject', 'content', 'isRead', 'created_at', 'updated_at']; public function recipient() { $this->hasOne('App\User', 'userID', 'recipientID'); } public function sender() { $this->hasOne('App\User', 'userID', 'senderID'); } public function getSenderUserNameAttribute() { $username = 'SnailLife'; if (isset($this->sender)) { $username = $this->sender->username; } return $username; } public function updateMessage($propertiesToUpdate) { $this->update($propertiesToUpdate); return true; }}



When a snail is killed or born we create a new message for the user. For example:


if ($this->isEgg) { $notification = [ 'recipientID' => $this->ownerID, 'subject' => 'An egg has died!', 'content' => 'Egg ID ' . $this->snailID . ' has died. Cause of death: ' . $cod ];}else { $notification = [ 'recipientID' => $this->ownerID, 'subject' => 'A snail (' . $this->snailID . ') has died!', 'content' => 'SnailID ID ' . $this->snailID . ' has died. Cause of death: ' . $cod ];}$notification = new UserMessage($notification);$notification->save();


(An egg is really just an instance of a snail, just one without a birthDate, so when an egg or snail dies it's handled in the same model).

Then there's the view. When logged in the user gets a notification of unread messages in the header:


@if (count(Auth::user()->unreadMessages) > 0) - You've got mail! @endif


Oh, we get unread messages in the User model using an Eloquent [font='courier new']hasMany[/font] relationship:

public function unreadMessages() { return $this->hasMany('App\UserMessage', 'recipientID', 'userID')->where('isRead', '=', 0);}

Once they click through they get taken to their message page (ignore the double-death messages. That's being fixed right now...):



Pretty simple and gets the job done for now.

Liza Shulyayeva

Liza Shulyayeva

 

Snaily Updates

I was going to try to remember all the things I've done in the last month on snails, but this seems impossible. So here's a lazy list of git commits within this time:

* 5 seconds ago, User messaging and cause of death [deploy:development]* 4 days ago, Some race view fixes, test out increasing AMR for wild snails since too little energy is being burnt during races. [deploy:development]* 4 days ago, Cancel races that have been unfinished for 24 hours, freeing up others to rent the race jar. [deploy:development]* 5 days ago, Remove old unneeded Minion and Latchet stuff [deploy:development]* 5 days ago, Remove unneeded logs, remove old breeding stuff that is no longer required with the brain [deploy:development]* 5 days ago, Fixes and cleanup, new racing jar in seeder [deploy:development]* 5 days ago, Remove start countdown from finished races, fix race result view [deploy:development]* 5 days ago, Fix race finish [deploy:development]* 5 days ago, Bug fixing in racing and memory saving [deploy:development]* 6 days ago, Admin option to return foreign snails to owners, get parent jar [deploy:development]* 6 days ago, Racing refactoring; stop destroying jars since we'll reuse them [deploy:development]* 7 days ago, More flexibility for historical event logging; correctly represent snails trying to mate with items [deploy:development[* 8 days ago, Substrate item creation fix [deploy:development]* 8 days ago, Gather snails to middle of jar, more exact movement, seed race jars for Mr Casinir [deploy:development]* 2 weeks ago, Ability to humanely euthanise eggs in jar [deploy:development]* 2 weeks ago, Some safeguards for user entering snail in race before it was returned from previous race [deploy:development]* 2 weeks ago, Replace finished and started columns with finishDate, startDate. Auto refresh jar page when race has started. [deploy:development]* 3 weeks ago, Fix move check for jar view [deploy:development]* 3 weeks ago, use View [deploy:development]* 3 weeks ago, Fix swallowing, isolate mood update [deploy:development]* 3 weeks ago, Resident register [deploy:development]* 3 weeks ago, Draw gravestone under snail pattern [deploy:development]* 3 weeks ago, Add rainbow bridge page [deploy:development]* 3 weeks ago, Display estimated race start time [deploy:development]* 3 weeks ago, Fix fertility spray [deploy:development]* 3 weeks ago, Fix bank link [deploy:development]* 3 weeks ago, Only list items user owns in closet [deploy:development]* 3 weeks ago, Fix for jar installation [deploy:development]* 3 weeks ago, Add bank hint to tutorial [deploy:development]* 3 weeks ago, Switch to Single log mode [deploy:development]* 4 weeks ago, CustomValidator [deploy:development]* 4 weeks ago, Stop comparing an item to itself... [deploy:development]* 4 weeks ago, User shouldn't be able to mix a substrate item with itself [deploy:development]* 4 weeks ago, Toggle disabling item action form elements on action select [deploy:development]* 4 weeks ago, Some item stuff [deploy:development]

I also decided to try live streaming Gastropoda development. I've only done a couple of sessions so far but it does help to keep me focused. Surprisingly enough a few people actually watch and ask questions. I didn't really think anyone would be that interested in watching someone code some weirdo snail app. It's nice because a lot of the "Have you thought about doing this" or "What about this" suggestions people have made are things I have thought of and/or already have implemented!

Liza Shulyayeva

Liza Shulyayeva

 

Items

After finishing yesterday's post about generating images for user-combined substrate items I realized that I never really provided an explanation about how items work in general (or how they work for now, anyway).

We have a few different item-associated models in Gastropoda. They are (model followed by associated table in parinetheses)

ItemType (item_types)
ItemTemplate (item_templates)
ItemUserTemplate (item_user_templates)
ItemNutrition (item_nutrition)
ItemUserNutrition (item_user_nutrition)
Item (items)

Item types are very generic. Right now I have the following item types:
consumable
decorative
substrate
jar
breeding
terrain
training
misc


ItemTemplates are pregenerated item...templates that can exist in the world. Note that these are not the existing items in the world, they are just things that could be instantiated into in-world items. Each item template is associated with an item type. Right now they are (template name, item type):
Lettuce Leaf (consumable)
Medium Sweet Potato (consumable)
Spark (decorative)
Basic Small Jar (jar)
Enthusiast Small Jar (jar)
7-Day Fertility Spray (breeding)
Grooved Glass 2mm (terrain)
Attractor (training)
Garden Dirt (substrate)
1kg Pure Fine Sand (substrate)
1kg Dry Oak Leaves (substrate)


ItemTemplates _can_ have an associated nutrition template. All consumables and substrate items have their own entries in the `item_nutrition` table.

Items are the actual instances of these item templates existing in the world. They are usually created when stores are restocked or when users mix their own substrate. An item entry consists of basic information about the instance, like:

itemID
templateID
ownerID
jarID
bitesTaken
posX
posY
isUserTemplate


Which brings me to user templates. Any mixing of substrate creates a new item template, or rather an ItemUserTemplate, and saves it in the [font='courier new']item_user_templates[/font] table. Substrate also gets its own nutrition entry, in [font='courier new']item_user_nutrition[/font]. This allows us to easily distinguish user generated items from basic items in the simulation and treat them differently. Then on an instance of an item, if [font='courier new']isUserTemplate[/font] is true, we know to look for associated templates in the user tables and not the regular tables.

That's pretty much it. The most annoying part about this setup is that there are so many inter-table dependencies. An item without a template entry is a broken item. A consumable without a template that has an associated nutrition record is broken. Etc...

Liza Shulyayeva

Liza Shulyayeva

 

Generating images for combined substrate

The Laravel 5 migration is complete and I'm back to substrate mixing and brain bug fixing!

Actually, I think now that basic mixing is done I'm going to do a few weeks of just bug fixing. The substrate has no effect on snails' attributes or behaviour yet, but it will. For now you can just mix different substrate items together into new substrate items.

I'm still not sure what the best way to create images for the custom-mixed substrate might be. The possibilities are pretty much endless - you can mix any substrate-type item with any other substrate-type item.

So what happens is - the user goes to their closet. They pick two substrate items to mix together. Then this whole mess happens:

protected function mixSubstrate() { $itemID1 = Input::get('itemID1'); $itemID2 = Input::get('itemID2'); $item1 = $this->findItem($itemID1); $item2 = $this->findItem($itemID2); $item1WeightG = $item1->template->weightG; $item2WeightG = $item2->template->weightG; $resultingItemName = Input::get('itemname'); $item1Attributes = $item1->template->nutrition->getAttributes(); $item2Attributes = $item2->template->nutrition->getAttributes(); $imageTitle = preg_replace('/\s+/', '', $resultingItemName) . '.png'; $fileToSaveTo = '/assets/img/items/terrain/custom/' . $imageTitle; $resultingImage = Utility::MergeTwoImages($item1->fullImagePath, $item2->fullImagePath, $fileToSaveTo); // Create resulting item template $resultingItemTemplate = new ItemUserTemplate(); $resultingItemTemplate->description = 'Custom item created by combining ' . $item1->template->name . ' and ' . $item2->template->name; $resultingItemTemplate->weightG = $item1WeightG + $item2WeightG; $resultingItemTemplate->name = $resultingItemName; $resultingItemTemplate->typeID = $item1->template->typeID; $resultingItemTemplate->ownerID = Auth::user()->userID; $resultingItemTemplate->imageSubdir = 'terrain/custom/'; $resultingItemTemplate->imageName = $imageTitle; $resultingItemTemplate->save(); // Create resulting item nutrition template $resultingItemNutrition = new ItemUserNutrition(); $resultingItemNutrition->templateID = $resultingItemTemplate->templateID; foreach ($item1Attributes as $key => $value) { if ($key !== 'templateID' && $key !== 'updated_at' && $key !== 'created_at') { $resultingItemNutrition->$key = $item1Attributes[$key] + $item2Attributes[$key]; } } $resultingItemNutrition->save(); // Create item of new template $resultingItemProperties = [ 'templateID' => $resultingItemTemplate->templateID, 'ownerID' => Auth::user()->userID, 'isUserTemplate' => 1 ]; $resultingItem = $this->createNewItem($resultingItemProperties); // Delete original items $item1->deleteItem(); $item2->deleteItem(); return Redirect::back()->with('resultingItem', $resultingItem);}
To actually create the image for the new item I added a new utility function:

public static function MergeTwoImages($imgPath1, $imgPath2, $fileToSaveTo = null) { $x = 50; $y = 50; $root = $_SERVER['DOCUMENT_ROOT']; $finalImage = imagecreatetruecolor($x, $y); imagesavealpha($finalImage, true); $sortedImages = [$imgPath1, $imgPath2]; sort($sortedImages); $image1Layer = imagecreatefrompng($root . $sortedImages[0]); $image2Layer = imagecreatefrompng($root . $sortedImages[1]); $opacity = 50; ImageCopyMerge($finalImage, $image1Layer, 0, 0, 0, 0, 50, 50, $opacity); ImageCopyMerge($finalImage, $image2Layer, 0, 0, 0, 0, 50, 50, $opacity); header('Content-Type: image/png'); if ($fileToSaveTo) { $fileToSaveTo = $root . $fileToSaveTo; imagepng($finalImage, $fileToSaveTo); } return $finalImage;}

The results look like the image next to this post (this example is experimenting with mixing 1kg oak leaves with 1kg sand).

It'll do for now. After some bug fixing I'll go back to substrate and start having it actually have an effect on the snails.

Liza Shulyayeva

Liza Shulyayeva

 

Migrating to Laravel 5

A few days ago I decided that there's no use putting it off any longer - it's time to upgrade to Laravel 5. I've been sitting on 4.2 for months and 5 is a major revision to...well...everything.

I knew this would be a large setback in terms of feature work. The migration would break things, things that would make me wish I'd never done it at all. In addition the plugins I'd been using for 4.2 would no longer be compatible with 5 (in fact, I don't think any of them are...)

I was using:
Confide for user authentication
Dispatcher for scheduled commands
Latchet for a Laravel-specific implementation of Ratchet.


The first two I just got rid of completely, using Laravel 5's authentication features and new command scheduling feature that does exactly what Dispatcher used to for me.

The last one I haven't even started on yet. It's going to be the toughest.

The most "breaking" update in Laravel 5 has been namespaces. I've been sitting here adding namespace declaration to all of my files. It's time consuming, but better in the long run and actually pretty relaxing. If this is the biggest problem I'll run into during this migration I'll be happy (Latchet aside, of course).

Anyway, within the next couple of days I hope to have the migration complete and functional. Then it's back to substrate and brains!

Liza Shulyayeva

Liza Shulyayeva

 

Thinking Through Substrate

After checking in the basic substrate item implementation yesterday I got to thinking - never a good sign.

The original idea was to have some pre-set types of substrate - garden dirt, rock, potting soil, pellets, whatever. But how would these items affect the snails and the jar? Would I need to invent some sort of set of attribute templates? Like "Oh well this garden dirt item is going to decrease jar temperature by 1C." and "Oh well some snails somehow don't like garden dirt and prefer other substrates". But why? Why would a snail prefer one template to another and how would this be decided?

Considering I'm already going for a lot of detail with consumables (making sure consumable items have macro and vitamin attributes based on their real life counterparts), I should maybe do the same thing for substrate. For example, garden dirt isn't just garden dirt...it can contain varying amounts of clay, humus, sand, rock, etc. An item called "Garden Dirt" would really be made up of multiple base elements in varying proportions. And that led me to thinking - while a template called "Garden Dirt" should exist and be available for pre-mixed purchase from the Habitat store, users should also be able to mix these elements themselves. As a user I want to buy my own sand, clay, rocks, whatever, and experiment with proportions of each to create substrates of different moisture, porosity, pH level, etc.

I don't know exactly how this is going to work yet - in fact from this point on I'm kind of making this up as I type. But here's a quick overview of how consumable items already work.

The database has the following item-related tables (with the specified columns):

item_types
typeID
name

item_templates
templateID
typeID
luminance
temperature
roughness
description
enduranceMod
speedMod
aggressionMod
modDurationMins
weightG
bites
baseDaysToRot
imageName
imageSubdir
rarity
basePrice
priorityLayer
pingsJar
isApplied

item_nutrition
templateID
carbohydrates
sugar
fat
protein
vitA
vitC
vitD
vitB6
vitB12
calcium
iron
magnesium

items
itemID
templateID
ownerID
jarID
deleted
bitesTaken
posX
posY

I suspect that I will need another table on the same "level" as [font='courier new']item_nutrition[/font]. Something like [font='courier new']item_terrain_elements[/font]...or something. It would contain the following columns (or something like this):

item_terrain_elements
templateID
pH
porosity
carbon (primary nutrient)
hydrogen (primary nutrient)
oxygen (primary nutrient)
nitrogen (primary macronutrient)
phosphorus (primary macronutrient)
potassium (primary macronutrient)
calcium (primary macronutrient)
magnesium (primary macronutrient)
sulfur (primary macronutrient)
iron (primary micronutrient)
manganese (primary micronutrient)
boron (primary micronutrient)
copper (primary micronutrient)
zinc (primary micronutrient)
molybdenum (primary micronutrient)
chloride (primary micronutrient)
nickel (primary micronutrient)
iodine (other)
fluorine (other)
selenium (other)
cobalt (other)
arsenic (other)
lithium (other)
chromium (other)
silicon (other)
tin (other)
vanadium (other)

The primary nutrients would affect plant growth and the "other" nutrients could affect the animals (ie snails) within the jar.

And looking at the above just now, I don't see a reason not to just add the above nutrients to the [font='courier new']item_nutrition[/font] table. Some of the columns (eg magnesium) would already be duplicates anyway...I could maybe just add the above columns to the nutrition table and set anything irrelevant to '0'...would there be something wrong with this? I don't know, I have to think.I wish somebody who actually knew what they were doing and wasn't making all this up as they went along could provide a second opinion.

Liza Shulyayeva

Liza Shulyayeva

 

Jar Substrate Is a Go

It's finally in. Well, the beginning stages anyway. Jar substrate. Anyone who knows anything about snails knows that they can't live in a glass box without some sort of substrate to hide in, get moisture from, and burrow under. So now you can apply one type of substrate (so far) to a jar. Eventually there will be different kinds of substrate, and maybe even substrate you can mix together from other types of substrate. Currently the substrate has no effect on the snails or the environment outside of being present and visually represented, but eventually it can impact everything from snail nutrients to jar temperature and what kinds of plants you can grow inside the jar. Here's how it works.

I added a new column to the [font='courier new']item_templates[/font] table: [font='courier new']isApplied[/font]. If an item template has this set to true it means that you apply it to a jar instead of placing it into the jar as a moveable item (like a potato, for example). Once you place substrate in you cannot drag it around, you can only remove it.

The substrate item has a terrain item type in the [font='courier new']item_types[/font] table. There are different actions available from the user's closet based on item type and sometimes template:


protected function getAvailableActionsAttribute() { $possibleActions = []; switch ($this->template->type->name) { case 'consumable': array_push($possibleActions, array('text' => 'Put in Jar', 'onclick' => "return showPutInJarForm($this->itemID)")); break; case 'decorative': array_push($possibleActions, array('text' => 'Put in Jar', 'onclick' => "return showPutInJarForm($this->itemID)")); break; case 'substrate': array_push($possibleActions, array('text' => 'Put in Jar', 'onclick' => "return showPutInJarForm($this->itemID)")); break; case 'jar': array_push($possibleActions, array('text' => 'Install Jar', 'onclick' => "return showInstallJarForm($this->itemID)")); break; case 'breeding': if(strpos($this->template->name,'Fertility Spray') !== false) { array_push($possibleActions, array('text' => 'Spray', 'onclick' => "return showPutInJarForm($this->itemID)")); }; break; case 'training': array_push($possibleActions, array('text' => 'Put in Jar', 'onclick' => "return showPutInJarForm($this->itemID)")); break; case 'terrain': array_push($possibleActions, array('text' => 'Apply to Jar', 'onclick' => "return showPutInJarForm($this->itemID)")); break; } return $possibleActions; }

So if the item type is 'terrain' the action text that appears is "Apply to Jar" vs "Put in Jar". It could call a different function when clicked as well, but in this case we handle the application step in the same function as the putting-in step so we show the same form.

So a user goes to their closet, scrolls to the Garden Dirt item, clicks "Apply to Jar", and selects which jar to apply the item to.

Then we update the item as we usually would by placing it inside the jar, except we only assign the item a position if [font='courier new']!$item->isApplied[/font]. At the end we run any special actions the item may have (which in this case it does) via [font='courier new']$item->runSpecialActions()[/font]:


case 'terrain': $jarController = new JarController(); $jar = $jarController->findJar($this->jarID); $jar->terrainItemID = $this->itemID; $propertiesToUpdate = [ 'terrainItemID' => $this->itemID ]; $jar->updateJar($propertiesToUpdate); break;

So the terrain gets applied. The item is not deleted until the substrate is thrown away. And if a jar has a terrain applied we draw the terrain tiles in the background:



Of course the next step is to have the substrate actually have some sort of effect. Snails housed in a jar without substrate should be affected by the lack of proper environment. Unfortunately right now I do not yet have snail health implemented. That is, snails can die of lack of energy via starvation, but they also need to be able to get ill from environmental and genetic factors and then die that way, too. The terrain will be just one of many things that should affect a snail's overall health and performance.

Liza Shulyayeva

Liza Shulyayeva

 

Tackling Gastropoda's memory usage - Match One

[font=arial]
I've been having to power cycle my Gastropoda Digital Ocean droplet every day lately because something was hogging up all the memory. It was a little annoying to diagnose because I know the problem had to do with the recurring events that are run using a cron job, of which there are quite a few. I know ideally PHP isn't something you would use for a long running server process, but these jobs weren't long running - they were just frequent. [/font]
[font=arial]
The problem ended up being something very obvious. Every minute I run an event called CheckIdleSnailActions. It loops through all living snails in the world and sees what they're doing. Unfortunately at some point that event began to take more than a minute to run. So when I ran ps aux --sort -rss to sort all running processes by memory usage I saw two instances of CheckIdleSnailActions running at the same time, one taking something like 60% memory and the other ~30%. [/font]
[font=arial]
I always knew running these checks every minute would see me encounter issues like these and know I need to find a more efficient way of doing this. In the meantime, the first step was to avoid running these events in parallel. [/font]
[font=arial]
To do this I ended up using lock files. The cron job runs a Dispatcher command called RunRecurringEvents. This command grabs all recurring events that are scheduled to run at this time from the database and runs them. These events include things like: [/font]
CheckIdleSnailActions
TickOrientationToNeutral
TickStressToNeutral
DepleteEnergyByTime
...etc
[font=arial]
I created a new directory in app/storage called lockFiles. [/font]
[font=arial]
I didn't want to stop all of these things from running in parallel for now. The heaviest event by far is CheckIdleSnailActions. This is where we generate the brain, where the neurons do their work for each snail, where we comb through each snail's memories for recognition of an object, retrieve and record mood impact, etc. [/font]
[font=arial]
When each recurring event is triggered it runs only if there is no lock file matching the event's name in app/storage/lockFiles. So if there is a file called app/storage/lockFiles/CheckIdleSnailActions.txt, the event will not run. If there is no lock file by this name it creates one and starts running, and when it finishes running it just deletes the lock file and allows the next instance of the event to run when it is triggered. [/font]
[font=arial]
So far I haven't had to power cycle the droplet since this went in. It's not a full solution (I can't even imagine how long it might take for one CheckIdleSnailActions event to run if the world ever has thousands of living snails in it), so this is a work in progress. [/font]

Liza Shulyayeva

Liza Shulyayeva

 

It's not so bad. We have snail babies, after all

I haven't been able to focus on snails much lately because for the past three weeks our kitten has been very very sick. He is in hospital two hours away and I can barely think about anything else. So while I'm working on little bits and pieces when I can, there hasn't really been anything blog-worthy (aside from some great new item art from Nicholas, but I want to have a "proper" post for that when I can manage to make myself sit down and focus on writing it). So I figured for now I'd repost a slightly older entry from my website that is still relevant today. I hope that's not against the site dev-journal rules.

It's not so bad. We have snail babies, after all

Sometimes I think about what I've been working on in my spare time since the beginning of last year and I'm like "What the crap, Liza. You're spending your time trying to make toy slugs. Go learn a useful skill or something."

The reason I keep getting drawn to simulations in the first place is the idea of virtual life, something with some sort of autonomy and - ok, I'll say it - sentience. I know I won't get to this point with my snails.

All aboard the robot train

[color=#ff0000]JakeThompson:[/color] Karinario told me that he likes you...but don't say that I told you ok?
[color=#0000ff]Guest14361:[/color] Ok
[color=#ff0000]JakeThompson:[/color] ok then....
[color=#0000ff]Guest14361:[/color] Do you hate life?
[color=#ff0000]JakeThompson:[/color] Can't say that I do...
[color=#0000ff]Guest14361:[/color] Mmk
[color=#ff0000]JakeThompson:[/color] *hits computer* stupid thing.
[color=#0000ff]Guest14361:[/color] Calm down
[color=#ff0000]JakeThompson:[/color] But I don't want to calm down
[color=#0000ff]Guest14361:[/color] Do you hate me?
[color=#ff0000]JakeThompson:[/color] Never! ure like the coolest person ive met so far
[color=#0000ff]Guest14361:[/color] Awe thank you
[color=#ff0000]JakeThompson:[/color] No prob.
[color=#0000ff]Guest14361:[/color] I'm ugly
[color=#ff0000]JakeThompson:[/color] Have you always been ugly?
[color=#0000ff]Guest14361:[/color] yes
[color=#ff0000]JakeThompson:[/color] Great...People say that I'm in denial....I wonder why that is...
[color=#0000ff]Guest14361:[/color] People suck
[color=#ff0000]JakeThompson:[/color] What's your favorite food?
[color=#0000ff]Guest14361:[/color] Pizza
[color=#ff0000]JakeThompson:[/color] uh huh
[color=#0000ff]Guest14361:[/color] What's you're fav food
[color=#ff0000]JakeThompson:[/color] Figure it out yourself.
[color=#0000ff]Guest14361:[/color] Pizza?
[color=#ff0000]JakeThompson:[/color] Before you go any further, can you make sure that you're actually making SENSE from now on?
[color=#0000ff]Guest14361:[/color] You're an asshole
[color=#ff0000]JakeThompson:[/color] That was mean.
[color=#0000ff]Guest14361:[/color] Bye!
[color=#ff0000]JakeThompson:[/color] Bye.
[color=#0000ff]Guest14361:[/color] Bye!
[color=#ff0000]JakeThompson:[/color] Stop repeating yourself.

When I was a teenager in Alabama I spent a couple of years working on this chat bot on a site called Personality Forge. Thousands of keyphrases and responses later I had a bot who could fool a few unlucky people into believing that he was a person for a little while (mostly if they catered the conversation to his training in vulgar themes, which were among the more popular topics with passing human speakers) and managed to place something like 13th in a yearly chat bot contest called The Chatterbox Challenge. But just like with snails, the idea that drew me to wanting to make a chat bot in the first place wasn't the thrill of having a program fake a conversation with a few hardcoded sayings. I guess I was excited about playing a game in which I simulated a truer simulation of life, AI twice removed.

I was trying to find out some old details about my bot on Google for this blog post and ran into this in my search - a snippet of a book in which he turns out to have been extremely briefly mentioned. It's pretty cool to see your bot's name in a book (I actually considered buying it because it sounds like a really interesting read, but it apparently costs $170 on Amazon - what?!), but the context is actually kind of what I'm ranting about here:[quote]
"Examples of the 'best' chatbot answers to the remaining eight questions in the round assessing 'regular' conversational systems, as awarded with maximum 4 points by Shah as judge in CBC 2005, are shown below (the name of the system is left of their response):
...
Do you like me?
Jake Thompson: One hundred percent!"[/quote]
[background=rgb(247,247,247)]-Creating Synthetic Emotions through Technological and Robotic Advancements by Jordi Vallverdu[/background]


After once again spending too long trying to remember my password to the Personality Forge, where the bot was made, I tried to dig up the response in his language center but could no longer find it - I must've made some change since the challenge. But here's how he would respond to a question of "Do you love me?", which would be pretty similar to how he'd have treated the above. You can see the original keyphrase affects his mood (asking the bot if he likes you will give him +1 to happiness and be treated as a high-importance phrase judging by the rank). In this case he wants the speaker to reveal their own feelings first and then answer based on this:


The whole bot is this. Hardcoded keyphrases and responses with some wildcards thrown in and some rudimentary "mood" magic. I don't know why most people make chat bots. Maybe the above generic response of "One hundred percent!" would be acceptable for some since it manages to not sound completely off-base in relation to the question, but when I saw my bot's replies to The Chatterbox Challenge judges' questions back in 2005, and the responses of bots who were much much more sophisticated than this, all I could really feel was disappointment. When you look back at the transcripts some may be amusing and it might be pretty cool when the bot manages to carry on some sort of conversation, but it's so clear that it's just a hunk of nothing underneath. At the time I wanted to make something more like ALICE, but even ALICE didn't live up to the thing that made me like the idea of chat bots.

If I wanted to make a more "proper" chat bot it would have to be a learning bot. Learning bots back then to me sounded fascinating and still do. Maybe, maybe one day a chat bot capable of learning could surpass its programming, is what my wishful thinking is telling me. At one point I was interested in trying to make one of those, but then I got distracted by other things and eventually by snails.

Fake it till it makes it?

At Interzone Games I got to work with a lot of really smart people. One of them was Jason Hutchens, who created a chat bot called MegaHAL (and before that HeX, who won the Loebner Prize Contest in 1996). I remember right before I had actually accepted the job offer (or maybe soon after) the whole team went out for drinks and we talked briefly about the idea of strong AI and intelligence in relation to robots. That's the first time I remember actually talking about this with anybody in person and it was interesting enough for me to still remember parts of the conversation despite the alcohol consumed that night. We talked about whether the definition of intelligence even matters - maybe if the bot is good enough to make people think it's intelligent, that qualifies it as intelligent. Maybe that's all you need. I mean, everyone I speak to in real life could be a robot, but I perceive them as intelligent, sentient beings because they manage to convince me. And that makes sense - if a bot can fool you into thinking it can think or is sentient or whatever it is that you hope it would be, maybe we should just accept that it is. But the real test is probably not a bot fooling some random external person interacting with it. I think the real test is the bot fooling its creator.

My Personality Forge chat bot could never have fooled me into thinking he's intelligent or sentient (even if he could fool other people...which he also can't). I know he's just a collection of preset phrases. I can talk to him and often remember exactly when I put in a response and why (usually I'd peruse transcripts, find parts where he'd get stuck, and add responses to those parts as I went - that's how his language expanded and became so questionably-themed). Even if I can't remember adding a certain phrase now, years down the line, all I have to do is go into his language center and do a search. The other thing is lack of change. My bot has been a 16 year old "human boy" since 2003. He never changed or evolved into anything. Life changes. In a true life simulation, the subjects have to change somehow.

So the snails

I guess the snails were a way for me to try to go one step beyond a rudimentary chatbot. Snails are considerably lower on the totem pole than humans in terms of intelligence, but I figured that would allow me to build in much more perceived depth and realism by simulating a simpler system better. It's not even artificial "intelligence" that I care so much about as opposed to artificial sentience. But just like with a chat bot, I don't think these snails are ever going to surprise me or convince me they're anything more than a bunch of hardcoded behaviours. It's the same thing, really, just in a different format. There are a few positive enhancements, like:
Unlike the chat bot, my snails do change. They are born, experience their jars, form memories which influence their behaviour based on those experiences, and die.
They do sometimes do things even I don't expect. But this tends to be a fluke - if I don't expect something it's probably a bug anyway and not an indicator of anything special or in any way truly autonomous.

So I have to make them better. I think the further I get with Gastropoda the less satisfied I become with the life side of things. Looking back it's painfully obvious that all of the layers I've been adding - visual and behavioural attributes, genetics, now the brain - have been weak attempts to make my own creation be able to surprise me.

But hey - it's not all hopeless. We have snail babies, after all.


(Outdated art)

Liza Shulyayeva

Liza Shulyayeva

 

On your mark, get set, crawl!

In my previous post I wrote about rethinking racing (aka ripping out the existing racing system).

Today I got most of the work done on that. There is a lot left to do, but as of right now:
A new jar gets created when a user hosts a race
When the race starts all entrants are placed into that jar in order of initiative, on the left end of the jar
An Attractor item is placed on the right end of the jar
The snails begin crawling toward the attractor item, their actions checked every minute just like in a normal jar. Snails can change direction and perform other actions live as this happens (and can be observed doing so)
As a snail touches the item its time is recorded and it is returned back to its parent jar in the owner's stable
When all snails finish and are relocated back to their owners' jars all items within the jar are deleted, and then the jar is deleted
The race is marked as "Finished" and results are displayed.
This has taught me that my way of estimating the snail's position (between its last current position and target position) is nowhere near accurate enough. The snail that was coming in first visually actually finished 2 seconds after another snail in the race. I'll need to work on this. Here are some screenshots:

Ready:


Go!


Why did this snail turn around?!


The last racer, straggling behind:


Race results:


Liza Shulyayeva

Liza Shulyayeva

 

Rethinking Racing. Also Cool Snail Art!

Racing in Gastorpoda has been implemented for months in its most basic state, but has never really gotten the attention it needs. So things like jars, in-jar positioning, and movement ended up evolving past the stage of the racing and making the current racing system kind of an outdated one.

Basically, right now racing is kind of simulating my simulation of jar movement. But really, I now have jar movement so I don't need to try to simulate it when racing. All I really have to do is create a jar, put the entrants in it, place an Attractor item at the other end, and watch them go.

Right now in racing the snails' steps are calculated based on time. So we basically loop through all the entrants until they've all either finished or dropped out and record their places as they come in. I would later be implementing stuff like snail interaction if two snails are side by side (one snail might try to bite the other, for example). But if I just make the whole race take place in an instance of a jar I don't have to worry about any of those things. The snails will interact naturally.

This does mean that I'll have to reimplement some attributes I used in current races to work in a jar context. Things like initiative (which dictates which snail starts moving faster), endurance (which, when depleted, causes a snail to be unable to move any farther and drop out). So it won't be a simple plug-in-a-jar-and-done thing just yet. I'm also creating a virtual user, Mr Casinir, who will own all of these racing jars (since we don't want any individual user hosting a race to have ownership, otherwise they'd be able to drag the snails and items around and ruin the race).

So I guess I'll be ripping out the whole current racing system and doing this instead.

Also - check out the cool new iteration of the snail by Nicholas Lives (who's also working on a game called We Need to go Deeper), who kindly offered to team up for art for Gastropoda. This is just the first iteration of a new snail and I already like it a thousand times better than the ugly placeholders I've been living with for over a year now.




Two of the snails have also mated, so I'm waiting for the eggs to hatch tomorrow morning if they don't cannibalise them by then.

Liza Shulyayeva

Liza Shulyayeva

 

Snail brain semi-stable and reviewing visual traits set during breeding

The snail brain, while still very basic, seems somewhat stable now. By "stable" I mean a snail can survive and reproduce without human intervention if it has a steady supply of food.

When I first embarked on the journey to power my snails' actions with a simple snail brain I threw together this diagram in the middle of the night:



Now it looks a little more like this:



Many things need to be refined and added on to. For example, right now a snail's stress increases if it is claustrophobic. However, all snails tend to react aggressively to stress - I need to implement a fight or flight response. Snails either attack other snails or try to run away from the crowding. I alraedy have attributes like fear and aggression (based on stress), but so far aggression is the only one that actually affects the snail's action.

I'm also starting to learn that I should trust the simulation a little bit more. In the past when something weird happend I knew it was most likely a bug of some sort. But now I've been running into more and more behaviours that, while odd, are in-line with what should actually be happening. I'll use this breeding as an example.

Two of my snails - Baloo and Lemur - mated. Baloo ate all of the eggs except for one because he was feeling claustrophobic. Here's what the remaining baby snail's family tree looks like:



However, as the baby grew and its shell color and pattern became more distinguishable I couldn't help but wonder...how did a snail baby coming from two very colorful looking snails end up so...prune-colored. Remember that the snails are hermaphrodites. The gender symbols below only display the role each snail took on in _that_ mating. Their orientation ticks back to neutral post-mating. A snail that mated as a female in one session could mate as a male in another (based on a few factors).



So I took a look at the parents. In this breeding Lemur bred as a male and Baloo bred as the female. Here are four of the visual traits (I'd say the major ones, although pattern radius also plays a big role) and how they came to be (the bolded pairs are the ones that actually got chosen for Lemur's Bagheera as it was being conceived and the values on the right are the final color values and pattern shape chosen for the baby):



And actually...it all kind of makes sense. I mean obviously I'm not going into the actual color-picking here (ie how you get a certain color value based on parents after a dominant allele is picked), but the general gist looks correct at first glance. I guess an ugly prune colored snail _can_ legitimately come from colorful parents.

So out of curiosity I took a look at the dead babies. You can't really see what they'd look like by eye since they died as eggs. _But_ due to a bug that I've since fixed, they continued to hatch after death and just stayed at a scale of ~0.00001 (so almost invisible to the naked eye). I used Chrome dev tools to increase their scale to 0.5 and check out what they would've looked like:



So...all pretty ugly. Either this is as it should be or the actual color value calculations make the colors more and more muddy each generation. I wish it was that pretty green one that survived. But that is not a mess I want to wade into at 11:51 at night.

Liza Shulyayeva

Liza Shulyayeva

 

Gastropoda - a snail simulation

My hobby project doesn't really fall into the category of a "game", but it seems game dev communities are still among the most relevant to discuss it. I guess that's because there are no popular simulation-specific communities that I've seen yet (but if you know of any, a link would be appreciated).

Gastropoda is a snail breeding and racing simulation written in PHP that I've been wanting to make for about 10 years. It's had several false starts over the years and this latest iteration began toward the beginning of last year. So far it's the longest surviving attempt. I hope that I have enough traction now to stick with it.

I started off making this in vanilla PHP and a few months ago transitioned to Laravel. I haven't used PHP much before. In my hobby projects I've preferred JS and at work it's mostly C#. But I always envisioned this as a PHP-based project, since the idea's conception, and that's how it's stayed (I even wrote a blog post about the choice).

I won't write too much of an introduction here because I've already explained it so much in different places. So here I'll copy and paste the general description of the project from my personal blog:

[indent=1]The simulation is based on real life and I'd like to make it as realistic as possible. It's not meant to be fun or a game (I imagine to most people this is going to be pretty boring and tedious, actually). However, I'm no snail expert or biologist, so I spend time hunting down online articles and research papers about snail life and nutritional requirements. Where I can't find the necessary information I base a feature on the requirements of humans until something better is found. For example, I found sources about recommended intake of some vitamins specific to snails, but not others. Those I didn't find any snail-specific information for I based on human vitamin requirements. Of course this is hugely wrong, but it lets me implement the features I need and then go back and tweak the values when I have more accurate information. If a planned feature can't realistically take place in the real world, it may need to be tweaked.

Here's an overview of what's currently implemented in one form or another:
User registration/login system
Catching of wild snails
Snail breeding
Fertility spray to allow snails to breed within sprayed jars
Snail decision making via snail brain (includes feeding, mating, biting so far)
Snail recognition and memories
Virtual currency
Virtual store
Purchasing/installing jars
Purchasing food
Scheduled food delivery service
Drag/drop snails and items to reposition within jar
Live view of snail positions on jar profile pages via Latchet
Snail racing
User race creation
Basic tutorial
Snail scouting (get exact speed, endurance, macro requirements, etc)
Snail relationship testing
Two food items (macros matching real life information)

Gastropoda priorities for this year:
Keep refining brain
Flesh out racing, lots to do here
Improve the actual site
Snail breeding and selling between users
Get deployment strategy sorted out (latest deployment woes coming up in a future post).

The website is super basic and crappy looking right now. Here are a few screenshots:










I blog pretty regularly about the progress and commits are autotweeted at @gastropodasim. I'll probably cross-post some of my existing blog posts here (that are still relevant to the project) and cross post the new ones.

Liza Shulyayeva

Liza Shulyayeva

Sign in to follow this  
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!