12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655 |
- // 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<option value=\"" + NullUUID + "\">Clean selection (let engine figure out next cube)</option>\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<option value=\"%s\">%s (%s/%s) [%s (%s,%s,%s)]</option>\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<option value=\"" + NullUUID + "\">Clean selection (let engine figure out next agent)</option>\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<option value=\"%s\">%s (%s) [%s (%s,%s,%s)]</option>\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("<hr />"),
- }
- 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 := "<nil>"
- 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) + "'<br />", "")
- 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 + "<br />", "")
- 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 <code>SIGCONT</code> to restart engine again<br />"," ")
- 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 <code>SIGCONT</code> to restart engine again<br />", 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 + "...</p>\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<br />", *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 := "<div class='table-responsive'><table class='table table-striped table-bordered table-hover'><caption>Obstacles</caption><thead><tr><th>#</th><th>Name</th><th>Position</th><th>Distance</th></tr></thead><tbody>\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("<tr><td>%v</td><td>%s</td><td>%v</td><td>%.4f</td></tr>\n", k, *point.Name.Ptr(), *point.Position.Ptr(), distance)
- if distance < smallestDistanceToObstacle {
- smallestDistanceToObstacle = distance
- nearestObstacle = point
- }
- }
- outputBuffer += "</tbody><tfoot><tr><th>#</th><th>Name</th><th>Position</th><th>Distance</th></tr></tfoot></table></div>\n"
- sendMessageToBrowser("status", "", outputBuffer, "")
- sendMessageToBrowser("status", "info", fmt.Sprintf("Nearest obstacle to agent %s: '%s' (distance: %.4f m)<br />", *Agent.Name.Ptr(), *nearestObstacle.Name.Ptr(), smallestDistanceToObstacle), "")
- // now pretty-print nearest cubes (20170806).
- outputBuffer = "<div class='table-responsive'><table class='table table-striped table-bordered table-hover'><caption>Cubes (Positions)</caption><thead><tr><th>UUID</th><th>Name</th><th>Position</th><th>ObjectType</th><th>Distance</th></tr></thead><tbody>\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("<tr><td>%v</td><td>%s</td><td>%v</td><td>%s</td><td>%.4f</td></tr>\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 += "</tbody><tfoot><tr><th>UUID</th><th>Name</th><th>Position</th><th>ObjectType</th><th>Distance</th></tr></tfoot></table></div>\n"
- sendMessageToBrowser("status", "", outputBuffer, "")
- sendMessageToBrowser("status", "info", fmt.Sprintf("Nearest %s cube to agent %s: '%s' (distance: %.4f m)<br />", 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><br/>", 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 "<br />\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<br />", 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<br />\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 . "</br>\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 . "</br>\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<br />\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: <br \>\n";
- var_dump($destCube);
- echo "<br />\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:<br />\n";
- var_dump($currentPosition);
- echo "Target Cube<br />\n";
- var_dump($targetCube);
- echo "<br />\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 <i>stopping</i>", "")
- }
- // 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 := "<div class='table-responsive'><table class='table table-striped table-bordered table-hover'><caption>" + popCaption + "</caption><thead><tr><th>Pop #</th><th>Fitness</th><th>Chromossomes</th></tr></thead><tbody>\n"
- for p, pop := range popul {
- outputBuffer += fmt.Sprintf("<tr><td>%v</td><td>%.4f</td>", p, pop.Fitness)
- for _, chr := range pop.chromosomes {
- outputBuffer += fmt.Sprintf("<td>(%v, %v, %v)<br />Distance: %.4f<br />Obstacle: %.4f<br />Angle: %.4f<br />Smoothness %.4f</td>",
- chr.x, chr.y, chr.z, chr.distance, chr.obstacle, chr.angle, chr.smoothness)
- }
- outputBuffer += "</tr>\n"
- }
- outputBuffer += "</tbody><tfoot><tr><th>Pop #</th><th>Fitness</th><th>Chromossomes</th></tr></tfoot></table></div>\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...<br />",
- 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<br />",
- 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<br />", 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<br />", 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()
- }
- }
|