// Here is the main engine app. package main import ( _ "github.com/go-sql-driver/mysql" "bytes" "database/sql" "encoding/json" "errors" "fmt" "github.com/jaytaylor/html2text" // converts HTML to pretty-printed text! (20170807) "github.com/spf13/viper" "golang.org/x/net/websocket" "gopkg.in/guregu/null.v3/zero" "html/template" "io/ioutil" "math" "math/rand" "net/http" "sort" "strings" "strconv" "sync/atomic" // used for sync'ing values across goroutines at a low level "time" ) // Define a communications procotol with the client, so that we can selectively // send messages to turn options on and off, etc. // Messages will be JSON. type WsMessageType struct { Type zero.String `json:"type"` SubType zero.String `json:"subtype"` Text zero.String `json:"text"` Id zero.String `json:"id"` } // New creates a new WsMessage out of 4 strings func (wsM *WsMessageType) New(msgType string, msgSubType string, msgText string, msgId string) *WsMessageType { wsM.Type = zero.StringFrom(msgType) wsM.SubType = zero.StringFrom(msgSubType) wsM.Text = zero.StringFrom(msgText) wsM.Id = zero.StringFrom(msgId) return wsM } // Constants for genetic algorithm. Names are retained from the PHP version. // TODO(gwyneth): Have these constants as variables which are read from the configuration file. const OS_NPC_SIT_NOW = "0" // Constants used in genetic algorithm. const RADIUS = 10.0 // this is the size of the grid that is thrown around the avatar const POPULATION_SIZE = 50 // was 50 const GENERATIONS = 20 // was 20 for 20x20 grid const CHROMOSOMES = 7 // was 28 for 20x20 grid const CROSSOVER_RATE = 90.0 // = 90%, we use a random number generator for 0-100 const MUTATION_RATE = 5.0 // = 0.005%, we use a random number generator for 0-1000 - TODO(gwyneth): try later with 0.01 const WALKING_SPEED = 3.19 // avatar walking speed in meters per second) // Weights for Shi & Cui const W1 = 1.0 // Sub-function of Path Length const W2 = 10.0 // Sub-function of Path Security const W3 = 5.0 // Sub-function of Smoothness // When transposing from the PHP version, we now cannot avoid having a few structs and types, since Go // is a strongly-typed language (20170726) // This was moved out of the GA code body because some external functions need those types (20170727) // chromosomeType is just a point in a path, really. type chromosomeType struct { x, y, z, distance, obstacle, angle, smoothness float64 } // popType represents each population as a list of points (= chromosomes) indicating a possible path; it also includes the fitness for this particular path. type popType struct { Fitness float64 chromosomes []chromosomeType } // movementJob is used in the worker goroutine which processes the points to move the avatars to, which needs to wait until the avatars have moved. // Go is so quick in recalculating generations that the avatars never get a chance to reach their destination until we wait for them! // So the commands to move the avatars need to go into a separate goroutine, to wait on avatars, while the main engine continues (20170730). type movementJob struct { agentUUID string // Agent UUID to move masterControllerPermURL string // masterController to use (note that the engine may pick one of several active ones) agentPermURL string // unfortunately the masterController cannot get or set Energy... destPoint chromosomeType // destination to go to; it's a chromosome so that we get distance information as well to calculate // for how long we need to sleep until the avatar reaches destination } // movementJobChannel is the blocking channel to which we write points for the next bot movement var movementJobChannel = make(chan movementJob, 1) // for now, we'll try with just 1 point // Go is tricky. While we send and receive WebSocket messages as it would be expected on a 'normal' // programming language, we actually have an insane amount of goroutines all in parallel. So what we do is to // send messages to a 'channel' (Go's version of a semaphore) and receive them from a different one; two sets // of goroutines will have their fun reading and sending messages to the client and updating the channels, // so other goroutines only send and receive to the channels and have no concept of 'WebSocket messages' // This is sort of neat because it solves parallelism (goroutines block on sockets) but it also allows // us to build in other transfer mechanisms and make them abstract using Go channels (20170703) var wsSendMessage = make(chan WsMessageType) var wsReceiveMessage = make(chan WsMessageType) var webSocketActive atomic.Value // this is an attempt to check if we have an active WebSocket, to avoid too many timeouts (20170728) // serveWs - this is what is 'called' from the outside, and I need to talk to a socket here. func serveWs(ws *websocket.Conn) { // see also how it is implemented here: http://eli.thegreenplace.net/2016/go-websocket-server-sample/ (20170703) var err error // to avoid constant redeclarations in tight loop below if ws == nil { Log.Panic("Received nil WebSocket — I have no idea why or how this happened!") } /* log.Printf("Client connected from %s", ws.RemoteAddr()) log.Println("entering serveWs with connection config:", ws.Config()) */ webSocketActive.Store(true) defer webSocketActive.Store(false) go func() { // log.Println("entering send loop") for { sendMessage := <-wsSendMessage if err = websocket.JSON.Send(ws, sendMessage); err != nil { Log.Error("Can't send; error:", err) break } } }() //log.Println("entering receive loop") var receiveMessage WsMessageType for { if err = websocket.JSON.Receive(ws, &receiveMessage); err != nil { Log.Error("Can't receive; error:", err) break } // Log.Debugf("Received back from client: type '%s' subtype '%s' text '%s' id '%s'\n", *receiveMessage.Type.Ptr(), *receiveMessage.SubType.Ptr(), *receiveMessage.Text.Ptr(), *receiveMessage.Id.Ptr()) wsReceiveMessage <- receiveMessage } } // convertLocPos converts a SL/OpenSim Location and Position into a single region name and (x,y,z) position coordinates func convertLocPos(location string, position string) (regionName string, xyz []string) { regionName = location[:strings.Index(location, "(")-1] coords := strings.Trim(position, "() \t\n\r") xyz = strings.Split(coords, ",") return regionName, xyz } // calcDistance calculates the distance between two points, which are actually arrays of x,y,z string coordinates. // TODO(gwyneth): Now that we have a strongly-typed language, we should create real objects for this. func calcDistance(vec1, vec2 []float64) float64 { deltaX := vec2[0] - vec1[0] // using extra variables because multiplication is probably deltaY := vec2[1] - vec1[1] // simpler than calling the math.Pow() function (20170725) deltaZ := vec2[2] - vec1[2] return math.Sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) } // engineHandler is still being implemented, it uses the old Go websockets interface to try to keep the page updated. func backofficeEngine(w http.ResponseWriter, r *http.Request) { // start gathering the cubes and agents for the Engine form checkSession(w, r) // Collect a list of existing bots and their PermURLs for the form db, err := sql.Open(PDO_Prefix, GoBotDSN) checkErr(err) // query for in-world objects that are cubes (i.e. not Bot Controllers) rows, err := db.Query("SELECT UUID, Name, ObjectType, ObjectClass, Location, Position FROM Positions WHERE ObjectType <> 'Bot Controller' ORDER BY Name") checkErr(err) defer rows.Close() var ( cubes, regionName = "", "" uuid, name, objType, objClass, location, position = "", "", "", "", "", "" xyz []string ) cubes = "\t\t\t\t\t\t\t\t\t\t\t\t\t\n" // As on backofficeCommands, but a little more complicated for rows.Next() { err = rows.Scan(&uuid, &name, &objType, &objClass, &location, &position) checkErr(err) // parse name of the region and coordinates regionName, xyz = convertLocPos(location, position) cubes += fmt.Sprintf("\t\t\t\t\t\t\t\t\t\t\t\t\t\n", uuid, name, objType, objClass, regionName, xyz[0], xyz[1], xyz[2]) } rows, err = db.Query("SELECT Name, UUID, Location, Position FROM Agents ORDER BY Name") checkErr(err) var uuidAgent, agentNames = "", "" agentNames = "\t\t\t\t\t\t\t\t\t\t\t\t\t\n" // To-Do: Agent options should also have location etc. // find all Names and OwnerKeys and create select options for each of them for rows.Next() { err = rows.Scan(&name, &uuidAgent, &location, &position) checkErr(err) regionName, xyz = convertLocPos(location, position) agentNames += fmt.Sprintf("\t\t\t\t\t\t\t\t\t\t\t\t\t\n", uuidAgent, name, uuidAgent, regionName, xyz) // not obvious for the Go linter, but xyz is an array of 3 elements (gwyneth 20210711) } rows.Close() // closing after deferring to close is probably not good, but I'll try it anyway (20170723) db.Close() tplParams := templateParameters{ "Title": "Gobot Administrator Panel - engine", "URLPathPrefix": template.HTML(URLPathPrefix), "Host": template.HTML(Host), "DestinationOptions": template.HTML(cubes), "AgentOptions": template.HTML(agentNames), "ServerPort": template.HTML(ServerPort), "Content": template.HTML("
"), } err = GobotTemplates.gobotRenderer(w, r, "engine", tplParams) checkErr(err) } // EngineRunning is the equivalent of a semaphore which starts or stops the engine. // OneStep allows the engine to run once, and then it stops. // These are an exported global (atomic) variables because we need to access it from the configuration function and from the SIGHUP/SIGCONT (20170811, 20170919). var EngineRunning, OneStep atomic.Value // engine does everything but the kitchen sink. // Notably, it does not only run the GA. It also deals on a separate goroutine with message handling for WebSockets, which also includes // the ability to start or stop the GA. And it launches another goroutine to deal with buffering commands to the virtual world. It really does // a lot, and possibly it ought to be simplified somehow. But this is the core, the essence, the kernel, the locus of all the rest! func engine() { // we use sync/atomic for making sure we can read a value that is set by a different goroutine // see https://texlution.com/post/golang-lock-free-values-with-atomic-value/ among others (20170704) var ( receiveMessage WsMessageType userDestCube atomic.Value // using sync/atomic to make values consistent among goroutines (20170704) curAgent atomic.Value ) // EngineRunning.Store(true) // we start by running the engine; note that this may very well happen before we even have WebSockets up (20170704) // now we let this be set via configuration file; the default is true; and a SIGHUP will start/stop the engine (20170811) userDestCube.Store(NullUUID) // we start to nullify these atomic values, either they will be changed by the user, curAgent.Store(NullUUID) // or the engine will simply go through all agents (20170725) OneStep.Store(false) // in theory, the engine starts or stops; one step is a special case if the client is connected (20170919) webSocketActive.Store(false) // as soon as we know that we have a connection to the client, we set this to true (20170728) sendMessageToBrowser("status", "info", "Entering the engine goroutine", "") // browser might not even know we're sending messages to it, so this will just gracefully timeout and be ignored and just appear on the log; changed message to display that we don't know if the engine is going to run or not (20170811) // Launch the movement worker goroutine. This is needed because Go is so fast calculating populations that it keeps giving the agents // contradictory movement commands. This uses a blocking channel and calculates how long the avatar needs to reach its destination // and sleeps for that time. There was something similar done in PHP as well, but PHP took long enough recalculating everything, so // it was deemed not to be necessary. (20170730) go movementWorker() // Now, this is a message handler to receive messages while inside the engine, we // block on a message and run a goroutine in the background, so we can safely continue // to run the engine without blocking or errors // I have no idea yet if this is a good idea or not (20170703) // At least it works (20170704) go func() { var messageType, messageSubType string for { receiveMessage = <-wsReceiveMessage if (receiveMessage.Type.Ptr() != nil) { messageType = *receiveMessage.Type.Ptr() } else { messageType = "empty" } if (receiveMessage.SubType.Ptr() != nil) { messageSubType = *receiveMessage.SubType.Ptr() } else { messageSubType = "empty" } switch messageType { case "status": switch messageSubType { case "ready": // this is what we get when WebSockets are established on the client webSocketActive.Store(true) // check for engine running or not and set the controls switch EngineRunning.Load().(bool) { case true: sendMessageToBrowser("htmlControl", "disable", "", "startEngine") sendMessageToBrowser("htmlControl", "disable", "", "oneStep") sendMessageToBrowser("htmlControl", "enable", "", "stopEngine") case false: sendMessageToBrowser("htmlControl", "enable", "", "startEngine") sendMessageToBrowser("htmlControl", "enable", "", "oneStep") sendMessageToBrowser("htmlControl", "disable", "", "stopEngine") default: // should never happen, but turn all buttons off just in case sendMessageToBrowser("htmlControl", "disable", "", "startEngine") sendMessageToBrowser("htmlControl", "disable", "", "oneStep") sendMessageToBrowser("htmlControl", "disable", "", "stopEngine") } case "gone": // The client has gone, we have no more websocket for this one (20170704) Log.Info("Client just told us that it went away, we continue on our own") webSocketActive.Store(false) default: // no other special functions for now, just echo what the client has sent... unknownMessage := "" if receiveMessage.Text.Ptr() != nil { unknownMessage = *receiveMessage.Text.Ptr() } Log.Warning("Received from client unknown status message with subtype", messageSubType, "text:", unknownMessage, " — ignoring...") } case "formSubmit": var messageText string if receiveMessage.Text.Ptr() != nil { messageText = *receiveMessage.Text.Ptr() } else { messageText = NullUUID + "|" + NullUUID // a bit stupid, we could skip this and do direct assigns, but this way we do a bit more effort wasting CPU cycles for the sake of code clarity (20170704) } returnValues := strings.Split(messageText, "|") userDestCube.Store(returnValues[0]) curAgent.Store(returnValues[1]) // Commented out because we know this works and we'll print it out later on anyway (20170730) // log.Println("Destination: ", userDestCube.Load().(string), "Agent:", curAgent.Load().(string)) // sendMessageToBrowser("status", "info", "Received '" + userDestCube.Load().(string) + "|" + curAgent.Load().(string) + "'
", "") case "engineControl": switch messageSubType { case "start": sendMessageToBrowser("htmlControl", "disable", "", "startEngine") sendMessageToBrowser("htmlControl", "disable", "", "oneStep") sendMessageToBrowser("htmlControl", "enable", "", "stopEngine") EngineRunning.Store(true) OneStep.Store(false) case "one-step": sendMessageToBrowser("htmlControl", "disable", "", "startEngine") sendMessageToBrowser("htmlControl", "disable", "", "oneStep") sendMessageToBrowser("htmlControl", "enable", "", "stopEngine") EngineRunning.Store(true) OneStep.Store(true) Log.Debug("OneStep is now", OneStep.Load().(bool)) /* case "stop": sendMessageToBrowser("htmlControl", "enable", "", "startEngine") sendMessageToBrowser("htmlControl", "enable", "", "oneStep") sendMessageToBrowser("htmlControl", "disable", "", "stopEngine") EngineRunning.Store(false) OneStep.Store(false) */ default: // anything else will stop the engine! sendMessageToBrowser("htmlControl", "enable", "", "startEngine") sendMessageToBrowser("htmlControl", "enable", "", "oneStep") sendMessageToBrowser("htmlControl", "disable", "", "stopEngine") EngineRunning.Store(false) OneStep.Store(false) } sendMessageToBrowser("status", "", "Engine " + messageSubType + "
", "") default: Log.Warning("Unknown message type", messageType) } } }() // We continue with engine. Things may happen in the background, and theoretically we // will be able to catch them. (20170703) // load whole database in memory. Really. It's so much faster that way! (20170722) var ( Agent AgentType // temporary way to store what comes from database lastAgentToRunUUID string // temporary storage of the last UUID agent that ran, when we pick one randomly, so we give others a chance (20170807) // Agents map[string]AgentType // we OUGHT to have a type without those strange zero.String, but it's tough to keep two structs in perfect sync (20170722); this is mapped by Agent UUID (20170725) Position PositionType Cubes map[string]PositionType // name to be compatible with PHP version; mapped by UUID (20170725). Object ObjectType Obstacles []ObjectType masterController PositionType // set to the most recent Bot Master Controller to send commands (name is the same as in former PHP code). ) // NOTE(gwyneth): The reason why we use maps and not slices (slices may be faster) is just because that way we can // directly address the element by UUID, instead of doing array searches (20170725) // prepare data to be saved as a CSV/XML file for later import into Excel and do nice graphics var export_rows []string // we place it here because of potential scope issues later on... // Theoretically endless loop follows (20170730) for { // Now, the problem with the approach of going through the list of Agents is that new Agents might appear, old // might be deleted, and then we're stuck! (Remember, the updating of the Agents table is done in parallel to this) // The idea of running goroutines for each Agent will also suffer from the same problem: what if the Agent dies and we don't know // about it? Of course we can check with a ping first. What about *new* Agents? How do we launch new goroutines for them if we // don't know about them beforehand? (20170801) // Second approach (20170801): initialise lastAgentRunning with NullUUID; pick one agent from the database; if it's the // same as before, pick a new one; if the user has provided us with an agent, use that one instead. This will at least provide // all agents with a chance of running, while allowing new Agents to appear and old ones to die (20170801). The cost of this // solution is that *some* Agents may not have a chance to run (since they're picked randomly), so we might be a little more // evil and use some magical pseudo-random generators from Go which allow a sequence of non-repeated random numbers to be // generated, and try to follow that order if possible, which means reloading the Agent table every cycle, but it might still be // worth it (20170801). // NOTE(gwyneth): From 20170807 onwards, the for loop runs forever, each cycle one Agent is picked to run // Note that the Agent table does not get reloaded each cycle, only a list of UUIDs, one of which is picked randomly and just one // Agent is loaded (20170807). if EngineRunning.Load().(bool) { // Open database // sanity check first, I have no idea why this happens sometimes: if PDO_Prefix == "" { PDO_Prefix = viper.GetString("gobot.PDO_Prefix") } if GoBotDSN == "" { GoBotDSN = viper.GetString("gobot.GoBotDSN") } db, err := sql.Open(PDO_Prefix, GoBotDSN) checkErr(err) defer db.Close() // needed? // load in Agents! We need them to call the movement algorithm for each one // BUG(gwyneth): what if the number of agents _change_ while we're running the engine? We need a way to reset the engine somehow. We have a hack at the moment: send a SIGCONT, it will try to restart the engine in a new goroutine // Changes 20170807: we now pick one agent randomly // First check if the end-user hasn't sent us an Agent UUID to use: userSetAgentUUID := curAgent.Load().(string) possibleAgentUUID := NullUUID // Log.Debug("userSetAgent is", userSetAgent) if userSetAgentUUID == NullUUID { // we need to pick one agent at random // Since apparenty MySQL is not very efficient at picking a row randomly, we load in a temporary number of UUIDs and // select one randomly in Go; then we just get the row from the database (20170807) rows, err := db.Query("SELECT UUID FROM Agents") if err != nil { // NOTE(gwyneth): caught that error when the grid is not operational yet! (20170816) sendMessageToBrowser("status", "error", fmt.Sprintf("Database error when selecting Agent to run: %v", err)," ") time.Sleep(10 * time.Second) continue // now we simply wait... } defer rows.Close() // needed? The problem here is with a continue on the check below... var agentUUIDs []string tempUUID := "" for rows.Next() { err = rows.Scan(&tempUUID) checkErr(err) agentUUIDs = append(agentUUIDs, tempUUID) } // if we have zero agents, we cannot go on! // TODO(gwyneth): be more graceful handling this, because the engine will stop forever this way // TODO(gwyneth): Better to randomly pick an agent from the database, and if none is available, skip a cycle (20170730). if len(agentUUIDs) == 0 { sendMessageToBrowser("status", "error", "Error: no Agents found. Engine cannot run. Aborted. Add an Agent and try sending a SIGCONT to restart engine again
"," ") time.Sleep(10 * time.Second) continue // now we simply wait... } // Log.Debug("We got a bunch of UUIDs:", agentUUIDs) possibleAgentUUID = agentUUIDs[0] // make sure we have at least a valid UUID!! // Generate a random index, search for it in agentUUIDs; if it's the same one as last time, try again; test for edge case, // i.e. that we have just 1 Agent in the database. (20170807) if len(agentUUIDs) > 1 && lastAgentToRunUUID != NullUUID { // edge case: on initialisation, both are set to NullID, so both are equal for index := 0; lastAgentToRunUUID == possibleAgentUUID; { index = rand.Intn(len(agentUUIDs)) possibleAgentUUID = agentUUIDs[index] //if lastAgentToRunUUID != possibleAgentUUID { // break //} // Log.Debug("Index picked:", index, "possibleAgentUUID", possibleAgentUUID, "Last agent was", lastAgentToRunUUID) } } } else { possibleAgentUUID = userSetAgentUUID sendMessageToBrowser("status", "info", fmt.Sprintf("Using agent UUID %s set by end-user", possibleAgentUUID), "") } lastAgentToRunUUID = possibleAgentUUID if possibleAgentUUID == NullUUID { Log.Critical("My logic is still borked!!") // NOTE(gwyneth): if this situation still happens, I need to revisit this! (20170813) } err = db.QueryRow("SELECT * FROM Agents where UUID=?", possibleAgentUUID).Scan( &Agent.UUID, &Agent.Name, &Agent.OwnerName, &Agent.OwnerKey, &Agent.Location, &Agent.Position, &Agent.Rotation, &Agent.Velocity, &Agent.Energy, &Agent.Money, &Agent.Happiness, &Agent.Class, &Agent.SubType, &Agent.PermURL, &Agent.LastUpdate, &Agent.BestPath, &Agent.SecondBestPath, &Agent.CurrentTarget, ) if err != nil || !Agent.OwnerKey.Valid { sendMessageToBrowser("status", "error", fmt.Sprintf("Error %v: no Agent found for UUID %s, or invalid OwnerKey for this agent. Engine cannot run. Aborted. Fix the database and try sending a SIGCONT to restart engine again
", err, possibleAgentUUID)," ") time.Sleep(10 * time.Second) continue // wait until situation improves... } // do the magic to extract the actual coords Agent.Coords_xyz = strings.Split(strings.Trim(*Agent.Position.Ptr(), "() \t\n\r"), ",") // we should extract the region name from Agent.Location, but I'm lazy! Log.Info("Starting to manipulate Agent", *Agent.Name.Ptr(), " (", *Agent.UUID.Ptr(), ")") // We need to refresh all the data about cubes and positions again! // do stuff while it runs, e.g. open databases, search for agents and so forth Log.Debug("Reloading database for Cubes (Positions) and Obstacles...") // Load in the 'special' objects (cubes). Because the Master Controllers can be somewhere in here, to save code. // and a database query, we simply skip all the Master Controllers until we get the most recent one, which gets saved // The rest of the objects are cubes, so we will need them in the ObjectType array (20170722). // BUG(gwyneth): Does not work across regions! We will probably need a map of bot controllers for that and check which one to call depending on the region of the current agent; simple, but I'm lazy (20170722). Cubes = make(map[string]PositionType) // clear array, let the Go garbage collector deal with the memory (20170723) rows, err := db.Query("SELECT * FROM Positions ORDER BY LastUpdate ASC") checkErr(err) for rows.Next() { err = rows.Scan( &Position.PermURL, &Position.UUID, &Position.Name, &Position.OwnerName, &Position.Location, &Position.Position, &Position.Rotation, &Position.Velocity, &Position.LastUpdate, &Position.OwnerKey, &Position.ObjectType, &Position.ObjectClass, &Position.RateEnergy, &Position.RateMoney, &Position.RateHappiness, ) checkErr(err) Position.Coords_xyz = strings.Split(strings.Trim(*Position.Position.Ptr(), "() \t\n\r"), ",") // check if we got a Master Bot Controller! if (*Position.ObjectType.Ptr() == "Bot Controller") { masterController = Position // this will get overwritten until we get the last, most recent one } else { Cubes[*Position.UUID.Ptr()] = Position // if not a controller, it must be a cube! add it to array! } } // we need at least ONE masterController, this will be nil if got none (20170807). if !masterController.PermURL.Valid { Log.Error(funcName() + ": Major error with database, we need at least one valid masterController to proceed. Sleeping for 10 seconds for user to correct this...") time.Sleep(10 * time.Second) continue // go to next iteration, this one has borked data (20170801) } // load in everything we found out so far on our region(s) but ignore phantom objects // end-users ought to set their cubes to phantom as well, or else the agents will think of them as obstacles! Obstacles = nil rows, err = db.Query("SELECT * FROM Obstacles WHERE Phantom = 0") checkErr(err) for rows.Next() { err = rows.Scan( &Object.UUID, &Object.Name, &Object.BotKey, &Object.BotName, &Object.Type, &Object.Position, &Object.Rotation, &Object.Velocity, &Object.LastUpdate, &Object.Origin, &Object.Phantom, &Object.Prims, &Object.BBHi, &Object.BBLo, ) checkErr(err) Object.Coords_xyz = strings.Split(strings.Trim(*Object.Position.Ptr(), "() \t\n\r"), ",") Obstacles = append(Obstacles, Object) } rows.Close() // Do not trust the database with the exact Agent position: ask the master controller directly // NOTE(gwyneth): Perhaps it's better to try asking the agent first, and if it refuses answering, try the master controller. (20170813) // I believe we go through the master controller because the agent might be too busy informing the database about sensor data. Log.Debug("master controller URL:", *masterController.PermURL.Ptr(), "Agent:", *Agent.Name.Ptr(), "Agent's OwnerKey:", *Agent.OwnerKey.Ptr()) // WHY Agent.Ownerkey?!?! Why not Agent.UUID?!?!? // The answer is NOT obvious: NPCs created by the master controller are owned by the avatar owning the master controller // and somehow to contact them we need the ownerkey, which is weird; newer versions of OpenSim are supposed to have fixed // this by adding a flag for NPCs not to be owned by anyone. Using this might mean to change a lot of code! (20170806) curPos_raw, err := callURL(*masterController.PermURL.Ptr(), "npc=" + *Agent.OwnerKey.Ptr() + "&command=osNpcGetPos") // NOTE(gwyneth): Apparently the web server will reply to ALL possible requests, even if the Agent doesn't exist any more; // I still don't know what to do in that situation, so we skip this cycle and try the next one (20170730). if curPos_raw == "" || curPos_raw == "No response could be obtained" || err != nil { Log.Error("Error in figuring out the response for agent", *Agent.Name.Ptr(), "so we will try to skip this cycle...") continue } sendMessageToBrowser("status", "info", "Grid reports that agent '" + *Agent.Name.Ptr() + "' is at position: " + curPos_raw + "...

\n", "") // update database with new position _, err = db.Exec("UPDATE Agents SET Position =? WHERE OwnerKey =?", strings.Trim(curPos_raw, " ()<>"), *Agent.OwnerKey.Ptr()) checkErr(err) db.Close() // sanitize Agent.Coords_xyz = strings.Split(strings.Trim(curPos_raw, " <>()\t\n\r"), ",") curPos := make([]float64, 3) // to be more similar to the PHP version Log.Debug("curPos_raw is", curPos_raw) _, err = fmt.Sscanf(curPos_raw, "<%f, %f, %f>", &curPos[0], &curPos[1], &curPos[2]) // best way to convert strings to floats! (20170728) checkErr(err) sendMessageToBrowser("status", "", fmt.Sprintf("Avatar '%s' (%s) raw position was %v; recalculated to: %v
", *Agent.Name.Ptr(), *Agent.Name.Ptr(), curPos_raw, curPos), "") // Now we select where to go to! // This will eventually become more complex and *possibly* part of the GA (20170811). // For now, we just see what attribute is more 'urgent' and choose a cube of the appropriate type. whatCubeTypeNext := "energy" // by default it will be energy // convert to floats, we could actually change that in the database but I'm lazy... (20170811) energyAgent, err := strconv.ParseFloat(*Agent.Energy.Ptr(), 64) checkErr(err) moneyAgent, err := strconv.ParseFloat(*Agent.Money.Ptr(), 64) checkErr(err) happinessAgent, err := strconv.ParseFloat(*Agent.Happiness.Ptr(), 64) checkErr(err) // Simple way to make a choice, but this will get much more complicated in the future (I hope!) (20170811) if moneyAgent < energyAgent { whatCubeTypeNext = "money" } if (happinessAgent < moneyAgent) && (happinessAgent < energyAgent) { whatCubeTypeNext = "happiness" } Log.Debug(*Agent.Name.Ptr(), "has energy:", energyAgent, "money:", moneyAgent, "happiness:", happinessAgent, "so obviously we will pick a", whatCubeTypeNext, "cube to move to.") // calculate distances to nearest obstacles and cubes // TODO(gwyneth): these might become globals, outside the loop, so we don't need to declare them var smallestDistanceToObstacle = 1024.0 // will be used later on var nearestObstacle ObjectType var smallestDistanceToCube = 1024.0 // will be used later on var nearestCube PositionType obstaclePosition := make([]float64, 3) cubePosition := make([]float64, 3) var distance float64 // pretty-print some nice tables for nearest obstacles and nearest cubes (20170806). outputBuffer := "
\n" for k, point := range Obstacles { _, err = fmt.Sscanf(*point.Position.Ptr(), "%f, %f, %f", &obstaclePosition[0], &obstaclePosition[1], &obstaclePosition[2]) checkErr(err) distance = calcDistance(curPos, obstaclePosition) outputBuffer += fmt.Sprintf("\n", k, *point.Name.Ptr(), *point.Position.Ptr(), distance) if distance < smallestDistanceToObstacle { smallestDistanceToObstacle = distance nearestObstacle = point } } outputBuffer += "
Obstacles
#NamePositionDistance
%v%s%v%.4f
#NamePositionDistance
\n" sendMessageToBrowser("status", "", outputBuffer, "") sendMessageToBrowser("status", "info", fmt.Sprintf("Nearest obstacle to agent %s: '%s' (distance: %.4f m)
", *Agent.Name.Ptr(), *nearestObstacle.Name.Ptr(), smallestDistanceToObstacle), "") // now pretty-print nearest cubes (20170806). outputBuffer = "
\n" for k, point := range Cubes { _, err = fmt.Sscanf(*point.Position.Ptr(), "%f, %f, %f", &cubePosition[0], &cubePosition[1], &cubePosition[2]) checkErr(err) distance = calcDistance(curPos, cubePosition) point.DistanceToAgent = distance // hope this works, we're saving the distance so that later on we can use this as a weight outputBuffer += fmt.Sprintf("\n", k, *point.Name.Ptr(), *point.Position.Ptr(), *point.ObjectType.Ptr(), point.DistanceToAgent) if distance < smallestDistanceToCube && *point.ObjectType.Ptr() == whatCubeTypeNext { smallestDistanceToCube = distance nearestCube = point } } outputBuffer += "
Cubes (Positions)
UUIDNamePositionObjectTypeDistance
%v%s%v%s%.4f
UUIDNamePositionObjectTypeDistance
\n" sendMessageToBrowser("status", "", outputBuffer, "") sendMessageToBrowser("status", "info", fmt.Sprintf("Nearest %s cube to agent %s: '%s' (distance: %.4f m)
", whatCubeTypeNext, *Agent.Name.Ptr(), *nearestCube.Name.Ptr(), smallestDistanceToCube), "") /* Idea for the GA: 1. Start with a 20x20 matrix (based loosely on Cosío and Castañeda) around the bot, which contain sensor data (we just sense up to 10 m around the bot). This might need adjustment (i.e. smaller size). - This represents the space of possible solutions - Active cube will determine attraction point (see later) - Chromosomes: randomly generated points (inside the 20x20 matrix) that the robot has to travel. Start perhaps with 50 with a length of 28 (Castañeda use 7 for 10x10 matrix). Points are bounded within the 20x20 matrix Now evaluate each chromosome with fitness function: - for each point: see if it's "too near" to an obstacle (potential collision) - ray casts are more precise, so give it a highest weight (not implemented yet) - normal sensor data give lower weigth - we can add modifiers: see number of prims of each obstacle (more prims, more weight, because object might be bigger than predicted); see if the obstacle is an agent (initially: agents might act as deflectors; later: interaction matrix will see if the bot goes nearer to the agent or avoids it) - for each point: see if it's closest to the cube. Lowest distance reduces weight. In theory, we wish to find the path with the least distance (less energy wasted) - sort chromosomes according to fitness - do 20 generations and find next expected point. Move bot to it. Reduce energy calculation on bot. See if it dies! - repeat for next bot position 20130520 — Results don't converge. It's hard to get the 'bot in less than a 10m radius. Attempt #2 - use a 10x10 matrix, just 7 points, like Castañeda Gotshall & Rylander (2002) suggest a population size of about 100-200 for 7 chromosomes Attempt #3 - Algorithm from Ismail & Sheta was badly implemented!! Attempt #4 - (to-do) implement Shi & Cui (2010) algorithm for fitness function Attempt #5 - Shi & Cui (2010) use a strange way to calculate path smoothness. Attempting Qu, Xing & Alexander (2013) which use angles. Modified mutation function, instead of the classical approach (switching two elements in the path), add random ±x, ±y to point André Neubauer (circular schema theorem, cited by Qu et al.) suggest two-point crossover Qu et al. suggest sorting path points, after crossover/mutation */ /* goal/target/attractor: where the 'bot is going to go next at some point, this ought to be included in the chromosome as well for now, we'll hard-code it (walk to the nearest cube) on stage two, we'll do a simple check: - see what attributes are lowest - go to the nearest cube that replenishes the attribute - since this will be iterated every time the 'bot moves, we hope it won't die from starvation, as moving elsewhere becomes prioritary */ // nearestCube is where we go (20140526 changing it to selected cube by user, named destCube) var destCube PositionType // BUG(gwyneth): Somehow, the code below will just be valid once! (20170728) - this needs more testing, I think // it was a `clear` somewhere at the end of the iteration, but we got to check it. Also, the submit button for // changing cube/agent does not go away and the visual feedback is weird (20170806). // Still working on it, it somehow works sometimes, but it's hard to debug because the GA does so many things (20170813). Log.Info("User-set destination cube for", *Agent.Name.Ptr(), ":", userDestCube.Load().(string), "(NullUUID means no destination manually set)") if userDestCube.Load().(string) != NullUUID { destCube = Cubes[userDestCube.Load().(string)] Log.Info("User has supplied us with a destination cube for", *Agent.Name.Ptr(), "named:", *destCube.Name.Ptr()) } else { destCube = nearestCube Log.Info("Automatically selecting nearest cube for", *Agent.Name.Ptr(), "to go:", *destCube.Name.Ptr()) } // This is just a test without the GA (20170725) // Commented out in 20170730 — forgot completely about this!! /* sendMessageToBrowser("status", "info", "GA will attempt to move agent '" + *Agent.Name.Ptr() + "' to cube '" + *destCube.Name.Ptr() + "' at position " + *destCube.Position.Ptr(), "") _, err = callURL(*masterController.PermURL.Ptr(), "npc=" + *Agent.OwnerKey.Ptr() + "&command=osNpcMoveToTarget&vector=<" + *destCube.Position.Ptr() + ">&integer=1") checkErr(err) */ time_start := time.Now() // Genetic algorithm for movement // generate 50 strings (= individuals in the population) with 28 random points (= 1 chromosome) at curpos ± 10m population := make([]popType, POPULATION_SIZE) // create a population; unlike PHP, Go has to have a few clues about what is being created (20170726) // Log.Debug("population len", len(population)) // initialise the slices of chromosomes; Go needs this to know how much memory to allocate (unlike PHP) for k := range population { population[k].chromosomes = make([]chromosomeType, CHROMOSOMES) // Log.Debug("Chromosome", k, "population len", len(population[k].chromosomes)) } // We calculate now the distance from each point to the destination // Because this is computationally intensive, we will not repeat it every time during each generation // Works well unless the destination moves! Then our calculations might be wrong // But we will catch up on the _next_ iteration (hopefully, unless it moves too fast) // We also use the best and second best path from a previous run of the GA // get from the database the last two 'best paths' (if it makes sense) start_pop := 0 // if we have no best paths, we will generate everything from scratch // Maybe it makes sense to keep around the last best paths if we're still moving towards the same // cube; so check for this first, and discard the last best paths if the destination changed // NOTE(gwyneth): Unlike the PHP version, the Go version deals simultaneously with an automated choice of path as well as manual // setting of destination, through user input; so the code here is slightly different. We *already* have the // destination cube in destCube (20170726). // We already have the cubePosition with the correct data (array of 3 float64 values for x,y,z). // NOTE(gwyneth): I have a doubt here, when the algorithm runs again, should the agent keep the CurrentTarget in mind? (20170726) // The PHP code seems to assume that, but it wasn't ready yet for automated runs... // calculate the center point between current position and target // needs to be global for path sorting function (Ruhe's algorithm) // NOTE(gwyneth): in PHP we had a global $centerPoint; Go uses capital letters to designate globality (20170726). // NOTE(gwyneth): Ruhe's algorithm is not used any more, so we can safely forget this declaration (20170805). /* CenterPoint := struct { x, y, z float64 }{ x: 0.5 * (cubePosition[0] + curPos[0]), y: 0.5 * (cubePosition[1] + curPos[1]), z: 0.5 * (cubePosition[2] + curPos[2]), } sendMessageToBrowser("status", "", fmt.Sprintf("Center point for this iteration is <%f, %f, %f>
", CenterPoint.x, CenterPoint.y, CenterPoint.z), "") */ // Now generate from scratch the remaining population for i := start_pop; i < POPULATION_SIZE; i++ { population[i].Fitness = 0.0 for y := 0; y < CHROMOSOMES; y++ { // Ismail & Sheta recommend to use the distance between points as part of the fitness // edge cases: first point, which is the distance to the current position of the agent // and last point, which is the distance between the last point and the target // that's why the first and last point have been inserted differently in the population // Log.Debug("i", i, "y", y) if y == 0 { // first point is (approx.) current position population[i].chromosomes[y].x = math.Trunc(curPos[0]) population[i].chromosomes[y].y = math.Trunc(curPos[1]) population[i].chromosomes[y].z = math.Trunc(curPos[2]) } else if y == (CHROMOSOMES - 1) { // last point is (approx.) position of target population[i].chromosomes[y].x = math.Trunc(cubePosition[0]) population[i].chromosomes[y].y = math.Trunc(cubePosition[1]) population[i].chromosomes[y].z = math.Trunc(cubePosition[2]) } else { // others are scattered around the current position population[i].chromosomes[y].x = math.Trunc(curPos[0] + (rand.Float64() * 2)*RADIUS - RADIUS) if population[i].chromosomes[y].x < 0.0 { population[i].chromosomes[y].x = 0.0 } else if population[i].chromosomes[y].x > 255.0 { population[i].chromosomes[y].x = 255.0 } population[i].chromosomes[y].y = math.Trunc(curPos[1] + (rand.Float64() * 2)*RADIUS - RADIUS) if population[i].chromosomes[y].y < 0.0 { population[i].chromosomes[y].y = 0.0 } else if population[i].chromosomes[y].y > 255.0 { population[i].chromosomes[y].y = 255.0 } population[i].chromosomes[y].z = math.Trunc((cubePosition[2] + curPos[2])/2) // will work for flat terrain but not more } // To implement Shi & Cui (2010) or Qu et al. (2013) we add these distances to obstacles together // If there are no obstacles in our radius, then we keep it clear population[i].chromosomes[y].obstacle = RADIUS // anything beyond that we don't care var point ObjectType for _, point = range Obstacles { _, err = fmt.Sscanf(*point.Position.Ptr(), "%f, %f, %f", &obstaclePosition[0], &obstaclePosition[1], &obstaclePosition[2]) checkErr(err) distance = calcDistance([]float64 {population[i].chromosomes[y].x, population[i].chromosomes[y].y, population[i].chromosomes[y].z }, obstaclePosition) // Shi & Cui and Qu et al. apparently just uses the distance to the nearest obstacle if distance < population[i].chromosomes[y].obstacle { population[i].chromosomes[y].obstacle = 1/distance // we use the inverse here, because if we have many distant obstacles it's // better than a single one that is close by } // TODO(gwyneth): obstacles flagged as ray-casting are far more precise, so they ought to be // more weighted. // TODO(gwyneth): obstacles could also have bounding box calculations: bigger objects should // be more weighted. However, HUGE objects might have holes in it. We ought to // include the bounding box only for ray-casting, or else navigation would be impossible! // Note that probably OpenSim raycasts only via bounding boxes (need confirmation) // so maybe this is never a good approach. Lots of tests to be done here! // NOTE(gwyneth): The latest version of llRayCast, v3 on BulletSim, does NOT use bounding boxes. Confirmed 20170730. } if RADIUS - population[i].chromosomes[y].obstacle < 0.00001 { // we use a delta to deal with rounding errors with floats population[i].chromosomes[y].obstacle = 0.0 } // calculate, for this point, its distance to the destination, currently $destCube // (exploded to array $cubePosition) // might not need this population[i].chromosomes[y].distance = calcDistance([]float64 { population[i].chromosomes[y].x, population[i].chromosomes[y].y, population[i].chromosomes[y].z}, cubePosition) // abandoned: initialize smoothness for Shi & Cui // adopted (20140523): smoothness using angles, like Qu et al. population[i].chromosomes[y].smoothness = 0.0 population[i].chromosomes[y].angle = 0.0 // maybe initialize it here } // endfor y // now sort this path. According to Qu et al. (2013) this gets us a smoother path // hope it's true, because it's computationally intensive // (20140523) we're using Ruhe's algorithm for sorting according to angle, hope it works // (20140524) Abandoned Ruhe, using a simple comparison like Qu et al. //echo "Before sorting, point i is: "; var_dump(population[i]); echo "
\n"; /* $popsort = substr(population[i], 1, -1); $first_individual = population[i][0]; $last_individual = population[i][CHROMOSOMES - 1]; usort($popsort, 'point_cmp'); population[i] = array_merge((array)$first_individual, $popsort, (array)$last_individual); */ pop := population[i].chromosomes sort.Slice(pop, func(a, b int) bool { // NOTE(gwyneth): these is still the old PHP comments, kept here for historical reasons // global $centerPoint; /* Attempt #1: Ruhe's algorithm $theta_a = atan2($a.y - $centerPoint.y, $a.x - $centerPoint.x); $angle_a = fmod(M_PI - M_PI_4 + $theta_a, 2 * M_PI); $theta_b = atan2($b.y - $centerPoint.y, $b.x - $centerPoint.x); $angle_b = fmod(M_PI - M_PI_4 + $theta_a, 2 * M_PI); if ($angle_a == $angle_b) return 0; return ($angle_a < $angle_b) ? -1 : 1; */ /* // Attempt #2: just angles if ($a.angle == $b.angle) return 0; return (abs($a.angle) < abs($b.angle)) ? -1 : 1; */ // Attempt #3: Just compare x,y! This is a terrible solution but better than nothing // using algorithm from Anonymous on http://www.php.net/manual/en/function.usort.php /* if ($a.x == $b.x) { if ($a.y == $b.y) { return 0; } elseif ($a.y > $b.y) { return 1; } elseif ($a.y < $b.y) { return -1; } } elseif ($a.x > $b.x) { return 1; } elseif ($a.x < $b.x) { return -1; } */ // Attempt #4: order by shortest distance to the target? return pop[a].distance < pop[b].distance }) } // endfor i // testing printing the current population (with json we get strange results!) // showPopulation(population, "Current population") // We're not finished yet! We need to calculate angles between all points (duh!) to establish smoothness // Let's do it from scratch: for i := 0; i < POPULATION_SIZE; i++ { population[i].chromosomes[0].angle = 0.0 // curPos has (obviously) angle 0 for j := 1; j < CHROMOSOMES; j++ { population[i].chromosomes[j].angle = math.Atan2(population[i].chromosomes[j].y - population[i].chromosomes[j-1].y, population[i].chromosomes[j].x - population[i].chromosomes[j-1].x) // sendMessageToBrowser("status", "", fmt.Sprintf("Pop %v, Chromosome %v - Angle is %v
", i, j, population[i].chromosomes[j].angle), "") } } // Initial population done; now loop over generations for generation := 0; generation < GENERATIONS; generation++ { // Calculate fitness // Log.Debug("Generating fitness for generation ", generation, " (out of ", GENERATIONS, ") for agent", *Agent.Name.Ptr(), "...") // When calculating a new population, each element will have its chromosomes reordered // So we have no choice but to calculate fitness for all population elements _again_ for i := 0; i < POPULATION_SIZE; i++ { fitnessW1 := 0.0 fitnessW2 := 0.0 fitnessW3 := 0.0 // note that first point is current location; we start from the second point onwards for y := 1; y < CHROMOSOMES; y++ { // Sub-function of Path Length (using Shi & Cui) distLastPoint := calcDistance([]float64 { population[i].chromosomes[y].x, population[i].chromosomes[y].y, population[i].chromosomes[y].z, }, []float64 { population[i].chromosomes[y-1].x, population[i].chromosomes[y-1].y, population[i].chromosomes[y-1].z, }) // Eduardo: suggests using square distance, means path will have more // distributed points. (20140704 - 2004) fitnessW1 += distLastPoint * distLastPoint // Sub-function of Path Security (using Shi & Cui) — obstacle proximity fitnessW2 += population[i].chromosomes[y].obstacle // Sub-function of Smoothness (using Shi & Cui) // This measures how zig-zaggy the path is, namely, if points are pointing back etc. // We want a smooth path towards the goal // Possibly here is where the weight will be added // Attempt #5: Qu et al. suggest the angle between line segments // used http://stackoverflow.com/questions/20395547/sorting-an-array-of-x-and-y-vertice-points-ios-objective-c /* Shi & Cui; abandoned population[i][$y]["smoothness"] = ( (population[i][$y-1].y - population[i][$y].y) / (population[i][$y-1].x - population[i][$y].x) ) - ( (population[i][$y].y - population[i][$y-1].y) / (population[i][$y].x - population[i][$y-1].x) ); */ // even though Shi & Cui was abandoned, we calculate smoothness nevertheless, only to make sure these calculations work! // (20170806) population[i].chromosomes[y].smoothness = ( population[i].chromosomes[y-1].y - population[i].chromosomes[y].y / population[i].chromosomes[y-1].x - population[i].chromosomes[y].x ) - ( population[i].chromosomes[y].y - population[i].chromosomes[y-1].y / population[i].chromosomes[y].x - population[i].chromosomes[y-1].x ) fitnessW3 += population[i].chromosomes[y].angle // clever, huh? check if abs makes sense // I don't think abs of an angle is a good idea (20170806) // and we'll also use the overall distance to the attractor //population[i]["fitness"] += population[i][$y]["distance"]; } // end for y population[i].Fitness = W1 * fitnessW1 + W2 * fitnessW2 + W3 * fitnessW3 } // end for i // note that the most critical point is the first: it's the one the 'bot will try to walk to. But we need // to calculate the rest of the path, too, which is "the best path so far which the bot plans to travel" // even if at every iteration, it will get calculated over and over again Log.Debug("CPU time used after fitness calculations for generation ", generation, ": ", time.Since(time_start)) /* showPopulation(population, fmt.Sprintf("Generation %v] - Before ordering:", generation)) */ // Now we do genetics! // To pick the 'best' population elements, we need to sort this by fitness, so that the best // elements are at the top // order by fitness sort.Slice(population, func(a, b int) bool { return population[a].Fitness < population[b].Fitness }) // TODO(gwyneth): to comment out later (20170727) showPopulation(population, fmt.Sprintf("Population for agent '%s' [generation %v] after calculating fitness and ordering by fitness follows:", *Agent.Name.Ptr(), generation)) sendMessageToBrowser("status", "", fmt.Sprintf("CPU time used after sorting generation %v for agent '%s': %v
\n", generation, *Agent.Name.Ptr(), time.Since(time_start)), "") // Selection step. We're using fitness rank newPopulation := make([]popType, POPULATION_SIZE) // create a new population; see comments above for k := range newPopulation { newPopulation[k].chromosomes = make([]chromosomeType, CHROMOSOMES) } // To introduce elitism, we will move the first 2 elements to the new population: newPopulation[0] = population[0] newPopulation[1] = population[1] // we could also delete the remaining two // for the remaining population: for i := 2; i < POPULATION_SIZE; i += 2 { // establish if we do crossover if rand.Float64() * 100 < CROSSOVER_RATE { // find a crossover point; according to André Neubauer, we might need two crossover points crossover_point := int(math.Trunc(rand.Float64() * CHROMOSOMES)) child0 := make([]chromosomeType, CHROMOSOMES) child1 := make([]chromosomeType, CHROMOSOMES) // Log.Debug("Generation ", generation, " - Crossover for ", i, " and ", (i + 1), " happening at crossover point: ", crossover_point) // now copy the chromosomes from the first parent, up to the crossover point, to child0 // and the remaining chromosomes go to the second child // simultaneously, do the reverse for the second parent // there are probably better/faster string manipulation techniques but this is easy to debug for chromosome := 0; chromosome < CHROMOSOMES; chromosome++ { if chromosome <= crossover_point { child0[chromosome] = population[i].chromosomes[chromosome] child1[chromosome] = population[i+1].chromosomes[chromosome] } else { child0[chromosome] = population[i+1].chromosomes[chromosome] child1[chromosome] = population[i].chromosomes[chromosome] } /* Log.Debug("Pop ", i, ", chromosome: ", chromosome, " Original chromosome: ", population[i].chromosomes[chromosome], "Child 0 chromosome: ", child0[chromosome]) */ } // endif chromosomes // test for mutation; note that this is permille and not percent if rand.Float64() * 1000 < MUTATION_RATE { /* Abandoned mutation implementation, which was a classical formulation for value-based GA (as opposed to bit-based) // pick two chromosomes for first child, two for second child // see http://obitko.com/tutorials/genetic-algorithms/crossover-mutation.php first_chromosome = mt_rand(0, CHROMOSOMES-1); second_chromosome = mt_rand(0, CHROMOSOMES-1); // exchange them, by using a temporary holder (this is mostly because // the exchange might be for the same chromosome! $temp_first_chromosome = child0[first_chromosome]; $temp_second_chromosome = child0[second_chromosome]; child0[first_chromosome] = $temp_second_chromosome; child0[second_chromosome] = $temp_first_chromosome; // echo "Generation " . $generation . " - Mutation happening for child " . i . " — exchanging chromosomes " . first_chromosome . " and " . second_chromosome . "
\n"; // same for second child first_chromosome = mt_rand(0, CHROMOSOMES-1); second_chromosome = mt_rand(0, CHROMOSOMES-1); // exchange them, by using a temporary holder (this is mostly because // the exchange might be for the same chromosome! $temp_first_chromosome = child1[first_chromosome]; $temp_second_chromosome = child1[second_chromosome]; child1[first_chromosome] = $temp_second_chromosome; child1[second_chromosome] = $temp_first_chromosome; // echo "Generation " . $generation . " - Mutation happening for child " . (i + 1) . " — exchanging chromosomes " . first_chromosome . " and " . second_chromosome . "
\n"; */ /* (20140523) Instead, as Qu et al. do, just pick a point and add some random distance to it */ first_chromosome := int(math.Trunc(rand.Float64() * (CHROMOSOMES-1))) second_chromosome := int(math.Trunc(rand.Float64() * (CHROMOSOMES-1))) child0[first_chromosome].x += (rand.Float64() * 2)*RADIUS/2 - RADIUS/2 if child0[first_chromosome].x < 0 { child0[first_chromosome].x = 0 } else if child0[first_chromosome].x > 255 { child0[first_chromosome].x = 255 } child0[first_chromosome].y += (rand.Float64() * 2)*RADIUS/2 - RADIUS/2 if child0[first_chromosome].y < 0 { child0[first_chromosome].y = 0 } else if child0[first_chromosome].y > 255 { child0[first_chromosome].y = 255 } child1[second_chromosome].x += (rand.Float64() * 2)*RADIUS/2 - RADIUS/2 if child1[second_chromosome].x < 0 { child1[second_chromosome].x = 0 } else if child1[second_chromosome].x > 255 { child1[second_chromosome].x = 255 } child1[second_chromosome].y += (rand.Float64() * 2)*RADIUS/2 - RADIUS/2 if child1[second_chromosome].y < 0 { child1[second_chromosome].y = 0 } else if child1[second_chromosome].y > 255 { child1[second_chromosome].y = 255 } } // endif mutation /* Log.Debug("Generation ", generation, " - New children for population ", i, ": ", child0, "\nand ", (i + 1), ": ", child1) */ /* we need to sort the points on the two childs AGAIN. Duh. And recalculate the angles. Duh, duh, duh */ /* child0sort = substr(child0, 1, -1); child1sort = substr(child1, 1, -1); $first_individual_child0 = child0[0]; $first_individual_child1 = child1[0]; $last_individual_child0 = child0[CHROMOSOMES - 1]; $last_individual_child1 = child1[CHROMOSOMES - 1]; usort(child0sort, 'point_cmp'); usort(child1sort, 'point_cmp'); child0 = array_merge((array)$first_individual_child0, child0sort, (array)$last_individual_child0); child1 = array_merge((array)$first_individual_child1, child1sort, (array)$last_individual_child1); */ sort.Slice(child0, func(a, b int) bool { return child0[a].distance < child0[b].distance }) sort.Slice(child1, func(a, b int) bool { return child1[a].distance < child1[b].distance }) child0[0].angle = 0.0; child0[1].angle = 0.0; for j := 1; j < CHROMOSOMES; j++ { child0[j].angle = math.Atan2(child0[j].y - child0[j-1].y, child0[j].x - child0[j-1].x) child1[j].angle = math.Atan2(child1[j].y - child1[j-1].y, child1[j].x - child1[j-1].x) } // add the two children to the new population; fitness will be calculated on next iteration newPopulation[i].chromosomes = child0 newPopulation[i+1].chromosomes = child1 // endif crossover } else { // no crossover, just move them directly newPopulation[i].chromosomes = population[i].chromosomes newPopulation[i+1].chromosomes = population[i+1].chromosomes // echo "No crossover for " . i . " and " . (i + 1) . " - moving parents to new population
\n"; } // Log.Debug("Pop ", i, "finished") } Log.Debug("Generation ", generation, " finished") population = newPopulation; // prepare population // Log.Debug("CPU time used after crossover and mutation up to generation ", generation, ": ", time.Since(time_start)) } // for generation //showPopulation(population, fmt.Sprintf("Final result (%v generation(s)):", GENERATIONS)) // at the end, the first point (after the current position) for the last population should give us the nearest point to move to // ideally, the remaining points should also have converged // obviously, as the avatar moves and finds about new obstacles etc. the population will change // move to target; integer=1 means "walking" (never "flying") // // Solution by Eduardo 20140704 — if we're close to the destination, within its radius, then we should // move to the last point — which is our current destination! // Calculate where we are before we move distanceToTarget := calcDistance(curPos, cubePosition) var target int // declared here for scope issues (PHP has the ternary operator for dealing with that, Go hasn't) if distanceToTarget < RADIUS { target = CHROMOSOMES } else { target = CHROMOSOMES -1 } sendMessageToBrowser("status", "info", fmt.Sprintf("Solution: move agent %s first to (%v, %v, %v) [and follow with %v points]. Distance is %.4f m", *Agent.Name.Ptr(), population[0].chromosomes[1].x, population[0].chromosomes[1].y, population[0].chromosomes[1].z, target - 1, distanceToTarget), "") // BUG(gwyneth): Major bug here! Possibly corrected with new approach. (20170805) // Basically, we generate the path for the next CHROMOSOME points. But we just need to move the avatar to the NEXT point // (i.e. chromosomes[1], because we will recalculate the whole path from then on. On the other hand, we need to print out // what the best path was so far, etc. and this will enter the calculations for the next batch of generations and so forth. // Nevertheless, the command to move the avatar is just for the NEXT point. (20170805) This should eliminate the 'quirks' of // the avatar moving in zig-zag and sometimes even backwards... and hopefully it will avoid them to move towards 0,0,Z... // Possibly Go is much faster at calculating new paths than the avatar is in moving to the next one. // because Go is so fast at calculating generations, we need to push the commands to give on a separate goroutine // which acts as a worker to consume points and wait on them until the avatar has finished walking to the point. // We begin with a channel with the capacity of allowing CHROMOSOMES points, maybe this needs to be adjusted in the future // Possibly we just need to move to the NEXT point, so the channel capacity would be just one! (20170805) movementJobChannel <- movementJob { agentUUID: *Agent.OwnerKey.Ptr(), masterControllerPermURL: *masterController.PermURL.Ptr(), agentPermURL: *Agent.PermURL.Ptr(), destPoint: population[0].chromosomes[1], } for p := 1; p < target; p++ { // (skip first point — current location) // This is added for the CSV export, but I don't know if it makes more sense here or in the movementWorker goroutine export_rows = append(export_rows, fmt.Sprintf("%f,%f,%f;", population[0].chromosomes[p].x, population[0].chromosomes[p].y, population[0].chromosomes[p].z)) } // Save two best solutions for next iteration; attempts to avoid to recalculate always from scratch // We do it after moving because the avatar needs a few seconds to reach destination // Reopen database, we need to write out the new Agent data db, err = sql.Open(PDO_Prefix, GoBotDSN) checkErr(err) // now update our database with the best paths and the target stmt, err := db.Prepare("UPDATE Agents SET BestPath=?, SecondBestPath=?, CurrentTarget=? WHERE UUID=?") if err != nil { sendMessageToBrowser("status", "error", fmt.Sprintf("%v: Updating database with best path, second best path, and current target for agent %s - prepare failed: %s", funcName(), *Agent.Name.Ptr(), err), "") } marshalled0, err := json.Marshal(population[0]) checkErr(err) marshalled1, err := json.Marshal(population[1]) checkErr(err) _, err = stmt.Exec(marshalled0, marshalled1, strings.Trim(*destCube.Position.Ptr(), " \t"), *Agent.UUID.Ptr()) if err != nil { sendMessageToBrowser("status", "error", fmt.Sprintf("%v: Updating database with best path, second best path, and current target for agent %s failed: %s", funcName(), *Agent.Name.Ptr(), err), "") } stmt.Close() db.Close() // See if we're close to the target; absolute precision might be impossible // first, read position again, just to see what we get curposResult, err := callURL(*masterController.PermURL.Ptr(), "npc=" + *Agent.OwnerKey.Ptr() + "&command=osNpcGetPos") if err == nil { sendMessageToBrowser("status", "info", fmt.Sprintf("Grid reports that agent %s is at position: %v", *Agent.Name.Ptr(), curposResult), "") } else { sendMessageToBrowser("status", "error", "We cannot get report from the grid for the position of agent " + *Agent.Name.Ptr(), "") } /* echo "This is how Destination Cube looks like:
\n"; var_dump($destCube); echo "
\n"; */ currentPosition := make([]float64, 3) // see comment above for doing the same to curPos (20170728) _, err = fmt.Sscanf(strings.Trim(curposResult, " ()<>"), "%f,%f,%f", ¤tPosition[0], ¤tPosition[1], ¤tPosition[2]) checkErr(err) /* echo "Distance comparison: Current Position:
\n"; var_dump($currentPosition); echo "Target Cube
\n"; var_dump($targetCube); echo "
\n"; */ distance = calcDistance(cubePosition, currentPosition) // this is how much is missing to reach destination if distance < 1.1 { // we might never get closer than this due to rounding errors sendMessageToBrowser("status", "info", fmt.Sprintf("Within rounding errors of %s, distance is merely %.4f m; let's sit %s down", *destCube.Name.Ptr(), distance, *Agent.Name.Ptr()), "") // if we're close enough, sit on it sitResult, err := callURL(*masterController.PermURL.Ptr(), "npc=" + *Agent.OwnerKey.Ptr() + "&command=osNpcSit&key=" + *destCube.UUID.Ptr() + "&integer=" + OS_NPC_SIT_NOW) if (err == nil) { sendMessageToBrowser("status", "info", "Result from " + *Agent.Name.Ptr() + " sitting: " + sitResult, "") } else { sendMessageToBrowser("status", "error", "Grid error when trying to sit " + *Agent.Name.Ptr(), "") } } else if distance < 2.5 { sendMessageToBrowser("status", "warning", fmt.Sprintf("%s is very close to %s, distance is now %.4f m", *Agent.Name.Ptr(), *destCube.Name.Ptr(), distance), "") } else { sendMessageToBrowser("status", "warning", fmt.Sprintf("%s is still %.4f m away from %s (%v, %v, %v)", *Agent.Name.Ptr(), distance, *destCube.Name.Ptr(), cubePosition[0], cubePosition[1], cubePosition[2]), "") } // Now place a button to save/export to CSV or XML // TODO(gwyneth): this was on the original code but no button was there; need to see where it is (20170728) // If the user had set agent + cube, clean them up for now // They never get cleaned! That's the whole point! Unless of course the user WANTS them cleaned (20170729) //userDestCube.Store(NullUUID) //curAgent.Store(NullUUID) sendMessageToBrowser("status", "info", fmt.Sprintf("CPU time used: %v", time.Since(time_start)), "") // output something to console so that we know this is being run in parallel /* fmt.Print("\r|") time.Sleep(1000 * time.Millisecond) fmt.Print("\r/") time.Sleep(1000 * time.Millisecond) fmt.Print("\r-") time.Sleep(1000 * time.Millisecond) fmt.Print("\r\\") time.Sleep(1000 * time.Millisecond) */ // if we're set to run only once then stop, change atomic values accordingly if OneStep.Load().(bool) { OneStep.Store(false) EngineRunning.Store(false) sendMessageToBrowser("htmlControl", "enable", "", "startEngine") sendMessageToBrowser("htmlControl", "enable", "", "oneStep") sendMessageToBrowser("htmlControl", "disable", "", "stopEngine") } } else { // stop everything!!! // in theory this is used to deal with reconfigurations etc. fmt.Print("\r𝔷") time.Sleep(1000 * time.Millisecond) fmt.Print("\rz") time.Sleep(1000 * time.Millisecond) fmt.Print("\rZ") time.Sleep(1000 * time.Millisecond) fmt.Print("\rℤ") time.Sleep(1000 * time.Millisecond) } } // end for (endless loop here) // Why should we ever stop? :) sendMessageToBrowser("status", "success", "this is the engine stopping", "") } // sendMessageToBrowser sends a string to the internal, global channel which is picked up by the websocket handling goroutine. // In the case of special status messages (info, success, warning, error) we also send the same message to the log. // If no WebSocket is active (and we check that in two different ways!) the message simply goes to the log instead. func sendMessageToBrowser(msgType string, msgSubType string, msgText string, msgId string) { text, err := html2text.FromString(msgText, html2text.Options{PrettyTables: true}) // prettify eventual HTML inside msgText checkErr(err) if webSocketActive.Load() != nil && webSocketActive.Load().(bool) { // no point in sending if nobody is there to receive var msgToSend WsMessageType msgToSend.New(msgType, msgSubType, msgText, msgId) // Go idiomatic programming: 'select' parallels the output of the two cases and picks the one which finishes; in this case, either // we are able to send a message via the channel, or there is a timeout, and Go picks what happens first select { case wsSendMessage <- msgToSend: // we use this so often as info/warning/error message that we may better send it also to the log if msgType == "status" && msgSubType != "" { switch msgSubType { case "info": Log.Info("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) case "notice": Log.Notice("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) case "success": Log.Notice("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) case "warning": Log.Warning("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) case "error": Log.Error("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) case "critical": Log.Critical("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) default: Log.Debug("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) } } else { Log.Debug("(connected via WebSocket)", msgType, "-", msgSubType, "-", text, "-", msgId) } // 'common' messages have the nil string subtype, so we ignore these and don't log them // we might have a Debug facility in the future which allows for more verbosity! case <-time.After(time.Second * 10): // this case exists only if we failed to figure out if the WebSocket is active or not; in most cases, we will // be able to know that in advance, but here we catch the edge cases. Log.Warning("WebSocket timeout after 10 seconds; coudn't send message:", msgType, "-", msgSubType, "-", text, "-", msgId) } } else { // No active WebSocket? Just dump it to the log. Note that this will be the most usual case, since we hardly expect users to be 24/7 in // front of their browsers... if msgType == "status" && msgSubType != "" { switch msgSubType { case "info": Log.Info("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) case "notice": Log.Notice("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) case "success": Log.Notice("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) case "warning": Log.Warning("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) case "error": Log.Error("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) case "critical": Log.Critical("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) default: Log.Debug("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) } } else { Log.Debug("(no WebSocket connection)", msgType, "-", msgSubType, "-", text, "-", msgId) } } } // callURL encapsulates a call to an URL. It exists as an analogy to the PHP version (20170723). func callURL(url string, encodedRequest string) (string, error) { // HTTP request as per http://moazzam-khan.com/blog/golang-make-http-requests/ body := []byte(encodedRequest) // Log.Debugf("%s: URL: %s Encoded Request: %s\n", funcName(), url, encodedRequest) rs, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewBuffer(body)) if err != nil { errMsg := fmt.Sprintf("HTTP call to %s failed; error was: '%v'", url, err) Log.Error(errMsg) return errMsg, err } defer rs.Body.Close() rsBody, err := ioutil.ReadAll(rs.Body) // Check for errors; if errors found, then send the error message back to the caller if err != nil { errMsg := fmt.Sprintf("error response from in-world object: '%v'", err) Log.Error(errMsg) return errMsg, err } else { if string(rsBody) == "No response could be obtained" { // weird case, but apparently it can happen! err = errors.New("No response could be obtained") } Log.Debugf("Reply from in-world object: '%s'; error was %v\n", rsBody, err) return string(rsBody), err } } // showPopulation is adapted from the PHP code to pretty-print a whole population // new version creates HTML tables func showPopulation(popul []popType, popCaption string) { if !ShowPopulation { // this might be the beginning of a debug level configuration type; currently we have the options from the go-logging pkg (20170813). return } outputBuffer := "
\n" for p, pop := range popul { outputBuffer += fmt.Sprintf("", p, pop.Fitness) for _, chr := range pop.chromosomes { outputBuffer += fmt.Sprintf("", chr.x, chr.y, chr.z, chr.distance, chr.obstacle, chr.angle, chr.smoothness) } outputBuffer += "\n" } outputBuffer += "
" + popCaption + "
Pop #FitnessChromossomes
%v%.4f(%v, %v, %v)
Distance: %.4f
Obstacle: %.4f
Angle: %.4f
Smoothness %.4f
Pop #FitnessChromossomes
\n" sendMessageToBrowser("status", "", outputBuffer, "") } // movementWorker reads one point from the movementJobChannel and sends a command to the avatar to move to it, and recalculates energy. // Then it blocks for the necessary amount of estimated time for the avatar to reach that destination until it reads the next // point. Right now, the channel accepts CHROMOSOME jobs at a time (20170730). // Finally, it calculates how much energy the avatar has spent for the distance travelled (20170811). // TODO(gwyneth): probably happiness is also affected, we have to think about a new formula for that (20170811). func movementWorker() { var nextPoint movementJob curPos := make([]float64, 3) // no need to allocate this over and over again newPos := make([]float64, 3) // this is the position that the avatar managed to travel to after this iteration for { // once started, never stops? nextPoint = <-movementJobChannel // consume one point from the channel // these must be set, nothing like a bit of error checking for eventual bugs elsewhere if nextPoint.masterControllerPermURL == "" || nextPoint.agentUUID == "" || nextPoint.agentPermURL == "" { continue // either the next job comes valid, or we continue to consume points until a valid one comes along } // The code below was commented in the PHP code, but we reuse it here as it was because it makes sense! (20170730) // How much should we wait? Well, we calculate the distance to the next point! And since avatars move pretty much // at the same speed all the time, we can make a rough estimate of the time they will take. // ask the avatar where it is curPosResult, err := callURL(nextPoint.agentPermURL, "command=osNpcGetPos") checkErr(err) if err != nil { // we have to assume this will never work, so we skip to the next case continue } // convert string result to array of floats _, err = fmt.Sscanf(strings.Trim(curPosResult, " ()<>"), "%f,%f,%f", &curPos[0], &curPos[1], &curPos[2]) checkErr(err) walkingDistance := calcDistance( curPos, []float64 { nextPoint.destPoint.x, nextPoint.destPoint.y, nextPoint.destPoint.z }) timeToTravel := walkingDistance / WALKING_SPEED // we might adjust this to assume the in-world calls took some time as well // do not wait too long, though! if timeToTravel > 5.0 { timeToTravel = 5.0 } sendMessageToBrowser("status", "", fmt.Sprintf("[%s]: Next point at %.4f metres; waiting %.4f secs for avatar %s to go to next point...
", funcName(), walkingDistance, timeToTravel, nextPoint.agentUUID), "") moveResult, err := callURL(nextPoint.agentPermURL, fmt.Sprintf("command=osNpcMoveToTarget&vector=<%v,%v,%v>&integer=1", nextPoint.destPoint.x, nextPoint.destPoint.y, nextPoint.destPoint.z)) checkErr(err) if err != nil { // we lost the ability to send messages; what to do now? Well, we can ignore this, the GA will // just require a new cycle continue } sendMessageToBrowser("status", "", fmt.Sprintf("[%s]: In-world result call from moving %s to (%v, %v, %v): %s
", funcName(), nextPoint.agentUUID, nextPoint.destPoint.x, nextPoint.destPoint.y, nextPoint.destPoint.z, moveResult), "") time.Sleep(time.Second * time.Duration(timeToTravel)) // ask the avatar AGAIN where it is, since it MIGHT not have reached the destination we expect it to reach (20170811). newPosResult, err := callURL(nextPoint.agentPermURL, "command=osNpcGetPos") checkErr(err) // convert string result to array of floats _, err = fmt.Sscanf(strings.Trim(newPosResult, " ()<>"), "%f,%f,%f", &newPos[0], &newPos[1], &newPos[2]) checkErr(err) travelled := calcDistance(newPos, curPos) // see how much we've actually travelled // calculate how much energy we've lost so far energyLost := travelled / WALKING_SPEED // some stupid formula, it doesn't matter, it's just to affect the counters // let the bloody avatar subtract some energy! energyResult, err := callURL(nextPoint.agentPermURL, "command=getEnergy") checkErr(err) energyAgent, err := strconv.ParseFloat(energyResult, 64) checkErr(err) sendMessageToBrowser("status", "", fmt.Sprintf("[%s]: %s had %f energy; lost %f on movement
", funcName(), nextPoint.agentUUID, energyAgent, energyLost), "") energyAgent -= energyLost energyResult, err = callURL(nextPoint.agentPermURL, fmt.Sprintf("command=setEnergy&float=%f", energyAgent)) checkErr(err) sendMessageToBrowser("status", "", fmt.Sprintf("[%s]: %s updated to new energy level %f; in.world reply: %v
", funcName(), nextPoint.agentUUID, energyAgent, energyResult), "") // update on database as well db, err := sql.Open(PDO_Prefix, GoBotDSN) checkErr(err) defer db.Close() stmt, err := db.Prepare("UPDATE Agents SET `Energy`=? WHERE OwnerKey=?") if (err != nil) { sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be prepared in database with new energy settings; database reply was: '%v'", nextPoint.agentUUID, err), "") } defer stmt.Close() execResult, err := stmt.Exec(energyAgent, nextPoint.agentUUID) if (err != nil) { sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in database with new energy settings; database reply was: '%v'", nextPoint.agentUUID, err), "") } else { id, err := execResult.LastInsertId() rowsAffected, err2 := execResult.RowsAffected() Log.Debug("Result from executing the energy update was:", id, err, "Rows affected:", rowsAffected, err2) } Log.Debug("Agent", nextPoint.agentUUID, "updated database with new energy:", energyAgent) stmt.Close() db.Close() } }