--Prevent standalone execution: if not TRAINSPORTED then print("To prevent players from harm, this file may only be executed by the trAInsported game.") return end --AI by Caldazar: Navigator= {} Memory = {} Controller = {} Train = {} Passenger = {} -- ----------------------------------------------------------------------------- Multiplier vars= {} vars.pathfinding= { ['direction'] = 4, ['node_distance'] = 2, ['deadend'] = 4, ['node'] = 0, ['crossroads'] = 2, ['loop'] = 4 -- multiplied with diameter of map } EvalPassengers= { ['remember'] = 1 } -- ----------------------------------------------------------------------------- trAInsporting main functions function ai.init(map, money) print ("Initiating modules") Navigator:init(map) if Memory.passengers == nil then Memory:init() end Controller:init() Train:init() print ("Creating global variables") nextPassenger= {} remLastChoice= {} bufferLastChoice= {} Controller:buyTrain() end function ai.enoughMoney(money) Controller:buyTrain() end function ai.newPassenger(name, x, y, destX, destY, vipTime) -- Check if Memory is already active if Memory.passengers == nil then Memory:init() end -- Save new passenger to Memory local tblPassenger= { ['name']= name, ['x']= x, ['y']= y, ['destX']= destX, ['destY']= destY, ['vipTime']= vipTime } Memory:rememberPassenger(tblPassenger) end function ai.foundPassengers(train, passengers) -- If no passenger is on board if train.passenger == nil then -- Save new passengers to Memory local i= 1 while i <= #passengers do local tblPassenger= passengers[i] tblPassenger['x']= train.x tblPassenger['y']= train.y Memory:rememberPassenger(tblPassenger) i= i + 1 end local idBestPass= Passenger:chooseBest(train, passengers) Memory:forgetPassenger(passengers[idBestPass].name) return passengers[idBestPass] end end function ai.foundDestination(train) print (train.name .. ": " .. train.passenger.name .. " gets off") bufferLastChoice= {} nextPassenger= Controller:nextPassenger(train) Train:forgerDirChoice(train) dropPassenger(train) end function ai.chooseDirection(train, directions) --forget oldest passenger Memory:forgetPassenger() -- Report the node to Navigator Navigator:reportNode(train.nextX, train.nextY) -- Let Train decide on the goal, depending if it has or searches a passenger local coordsGoal= Train:getGoal(train) -- Let Navigator evaluate the direction values local tblValues= Navigator:evalDirs(train, directions, coordsGoal) -- Return the direction to take max, min= maxmin(tblValues) -- print(min.." - "..max) for dir, value in pairs(tblValues) do if value == max then print(train.name..": \"Going "..dir.." (value: "..value..")\"") Train:remDirChoice(train, dir) return dir end end end function ai.blocked(train, possibleDirections, prevDirection) if prevDirection == "N" then -- if i've tried North before, then try South, then East, then West if possibleDirections["S"] == true then return "S" elseif possibleDirections["E"] == true then return "E" elseif possibleDirections["W"] == true then return "W" else return "N" end elseif prevDirection == "S" then if possibleDirections["E"] == true then return "E" elseif possibleDirections["W"] == true then return "W" elseif possibleDirections["N"] == true then return "N" else return "S" end elseif prevDirection == "E" then if possibleDirections["W"] == true then return "W" elseif possibleDirections["N"] == true then return "N" elseif possibleDirections["S"] == true then return "S" else return "E" end else if possibleDirections["N"] == true then return "N" elseif possibleDirections["S"] == true then return "S" elseif possibleDirections["E"] == true then return "E" else return "W" end end end -- ***************************************************************************** CONTROLLER function Controller:init() --print("Controller is active!") end function Controller:buyTrain() local houses= Navigator:search("H") print("Controller: Bought new train and place it at ".. houses[1].x ..":".. houses[1].y) buyTrain(houses[1].x, houses[1].y) end function Controller:nextPassenger(train) -- -- get nearest Passenger local nearest_distance= 512 for name,table in pairs(Memory.passengers) do local tmpDistance= distance(train.x, train.y, table.x, table.y) if tmpDistance < nearest_distance then nearest_distance= tmpDistance nextPassenger= name end end local chosenPassenger= Memory.passengers[nextPassenger] if chosenPassenger then print("Go for ".. nextPassenger .." at ".. chosenPassenger.x ..":".. chosenPassenger.y) end return Memory.passengers[nextPassenger] end -- ***************************************************************************** MEMORY function Memory:init() self.directions= {} self.passengers= {} self.nodes= {} --print("Memory is active") self.maxCount= Eval.diameter(EvalPassengers.remember) print("Memory: Maximum passenger memory is "..self.maxCount) end function Memory:rememberNode(x, y, node) if Memory.nodes[x] == nil then Memory.nodes[x]= {} end self.nodes[x][y]= node end function Memory:getNode(x, y) if Memory.nodes[x] ~= nil and Memory.nodes[x][y] ~= nil then return Memory.nodes[x][y] end end function Memory:rememberPassenger(tblPassenger) -- forget Passengers that overflow the max-count of index -- If passenger is already memorized, renew his entry local boolMemorized= false local i= 1 while i <= #self.passengers do if self.passengers[i].name == tblPassenger.name then table.remove(self.passengers, i) break end i= i+1 end table.insert(self.passengers, 1, tblPassenger) -- if table length is over max, remove the last (oldest) entry if #self.passengers > self.maxCount then table.remove(self.passengers) end end function Memory:forgetPassenger(name) if name == nil and #self.passengers >= (self.maxCount / 2) then table.remove(self.passengers) else local i= 1 while i <= #self.passengers do if self.passengers[i].name == name then table.remove(self.passengers, i) break end i= i+1 end end end -- ***************************************************************************** Navigator function Navigator:init(map) self.map= map self.moves= { ['N']= {['x']= 0, ['y']= -1}, ['E']= {['x']= 1,['y']= 0}, ['S']= {['x']= 0,['y']= 1}, ['W']= {['x']= -1,['y']= 0} } --print("Navigator representation is active!") end function Navigator:reportNode(x, y) -- If this node is not already saved create the table and send it to Memory if Memory:getNode(x, y) == nil then print("Navigator: New node at "..x..":"..y.." reported!") local node= Navigator:createNodeInfo(x, y) Memory:rememberNode(x, y, node) end end function Navigator:createNodeInfo(x, y) -- Create local table local node= {} -- Get possible directions local arrDirections= Navigator:getDirections(x, y) -- Create tables of neighbouring nodes for i=1, #arrDirections, 1 do local coordsFrom= {['x']= x, ['y']= y } local coordsAt= { ['x']= x + self.moves[arrDirections[i]].x, ['y']= y + self.moves[arrDirections[i]].y } local coordsNextNode= Navigator:nextNode(coordsAt, coordsFrom) local tblNextNode= { ['x']= coordsNextNode.x, ['y']= coordsNextNode.y, ['value']= Navigator:evaluateNextNode(coordsNextNode.type) } node[arrDirections[i]]= tblNextNode end -- Return the new node return node end function Navigator:evalDirs(train, directions, coordsGoal) -- Get global direction evaluation local tblNode= Memory:getNode(train.nextX, train.nextY) -- Check which of the node's direction options the train can use local tblTrainDirs= {} for dir, tblNodeInfo in pairs(tblNode) do if directions[dir] ~= nil then tblTrainDirs[dir]= tblNodeInfo end end -- Calculate the values for the direction of this node and the target local tblDistance= Navigator:evalDistance(train, tblTrainDirs, coordsGoal) -- Calculate the values for the distances of the next nodes and the target local tblNodeDistances= {} for dir, tblDir in pairs(tblTrainDirs) do tblNodeDistances[dir]= distance(tblDir.x, tblDir.y, coordsGoal.x, coordsGoal.y) tblNodeDistances[dir]= tblNodeDistances[dir] * vars.pathfinding.node_distance tblNodeDistances[dir]= -math.ceil(tblNodeDistances[dir]) end -- Add the evaluations to the node table local tblReturn={} for dir, value in pairs(tblTrainDirs) do -- Avoid being stuck in a loop local numLoop= Train:checkDirChoice(train.ID, train.nextX, train.nextY, dir) --print(dir..": "..type(tblTrainDirs[dir].value).."+"..type(tblDistance[dir]).."+"..type(tblNodeDistances[dir])) tblReturn[dir]= tblTrainDirs[dir].value + tblDistance[dir] + tblNodeDistances[dir] + numLoop print(dir..": "..tblTrainDirs[dir].value.."+"..tblDistance[dir].."+"..tblNodeDistances[dir].."+"..numLoop.." = "..tblReturn[dir]) end -- Return the evaluation table for the train to choose from return tblReturn end function Navigator:evalDistance(train, directions, coordsGoal) -- subtract node from goal local x= coordsGoal.x - train.nextX local y= coordsGoal.y - train.nextY -- multiply values x= x * vars.pathfinding.direction y= y * vars.pathfinding.direction -- create and evaluate dir table local tblDirs= {['N']= -y, ['E']= x, ['S']= y, ['W']= -x} --clean of non available options for dir, bool in pairs(tblDirs) do if directions[dir] == nil then tblDirs[dir]= nil end end -- return dir table with values return tblDirs end function Navigator:nextNode(coordsAt, coordsFrom) -- Get all dirs of this rail tblDir= Navigator:getDirections(coordsAt.x, coordsAt.y) -- if it's just a rail do the same check for the following rail if #tblDir == 2 then for i=1, #tblDir, 1 do tblDirMove= self.moves[tblDir[i]] -- if it's not from where I'm coming from (except for the first move) if coordsAt.x + tblDirMove.x ~= coordsFrom.x or coordsAt.y + tblDirMove.y ~= coordsFrom.y then local coordsNext= {['x']= coordsAt.x + tblDirMove.x, ['y']=coordsAt.y + tblDirMove.y} local returnNode= Navigator:nextNode(coordsNext, coordsAt) return returnNode end end -- if it's a node, return its coordinates table elseif #tblDir == 1 or #tblDir == 3 or #tblDir == 4 then local tblPrints={"dead-end", "", "node", "crossroads"} print("Navigator: Connects to "..tblPrints[#tblDir].." at "..coordsAt.x..":"..coordsAt.y) local tblNode={['x']= coordsAt.x, ['y']= coordsAt.y, ['type']= #tblDir} return tblNode end end function Navigator:evaluateNextNode(intType) if intType == 3 then return vars.pathfinding.node elseif intType == 1 then return -vars.pathfinding.deadend elseif intType == 4 then return vars.pathfinding.crossroads end end function Navigator:getDirections(x, y) local arrDirections= {} if self.map[x][y] == "C" then local count= 0 if self.map[x][y-1] == "C" then table.insert(arrDirections, "N") end if self.map[x+1][y] == "C" then table.insert(arrDirections, "E") end if self.map[x][y+1] == "C" then table.insert(arrDirections, "S") end if self.map[x-1][y] == "C" then table.insert(arrDirections, "W") end end --print(#arrDirections.." connections at "..x..":"..y) return arrDirections end function Navigator:search(itemtype) local listItems= {} listItems[itemtype]= {} for x = 1, self.map.width, 1 do for y = 1, self.map.height, 1 do if self.map[x][y] == itemtype then local item= {['x']= x, ['y']= y} table.insert(listItems[itemtype], item) --print("Navigator: Found ".. itemtype .." at".. x ..":".. y) end end end if #listItems[itemtype] == 0 then item= {['x']= 1, ['y']= 1} table.insert(listItems[itemtype], item) end return listItems[itemtype] end -- ***************************************************************************** PASSENGER function Passenger:init() end function Passenger:chooseBest(train, passengers) -- get passenger with the shortest way local bestPassID= 1 local minDist= 512 local i= 1 while i <= #passengers do local tmpDist= distance(train.x, train.y, passengers[i].destX, passengers[i].destY) if tmpDist < minDist then minDist= tmpDist bestPassID= i end i= i+1 end return bestPassID end -- ***************************************************************************** EVAL Eval= {} function Eval.diameter(multiplier) return math.ceil(distance(1, 1, Navigator.map.width, Navigator.map.height) * multiplier) end -- ***************************************************************************** TRAIN function Train:init() self.choices= {} end function Train:remDirChoice(train, dir) if self.choices[train.ID] == nil then self.choices[train.ID]= {} end if self.choices[train.ID][train.nextX] == nil then self.choices[train.ID][train.nextX]= {} end if self.choices[train.ID][train.nextX][train.nextY] == nil then self.choices[train.ID][train.nextX][train.nextY]= {} end if self.choices[train.ID][train.nextX][train.nextY][dir] == nil then self.choices[train.ID][train.nextX][train.nextY][dir]= 0 end local numSubtraction= Eval.diameter(vars.pathfinding.loop) self.choices[train.ID][train.nextX][train.nextY][dir]= self.choices[train.ID][train.nextX][train.nextY][dir] - math.floor(numSubtraction) end function Train:checkDirChoice(id, nextX, nextY, dir) if self.choices[id] ~= nil and self.choices[id][nextX] ~= nil and self.choices[id][nextX][nextY] ~= nil and self.choices[id][nextX][nextY][dir] ~= nil then return self.choices[id][nextX][nextY][dir] end return 0 end function Train:forgerDirChoice(train) if self.choices[train.ID] ~= nil then self.choices[train.ID]= nil end end function Train:getGoal(train) -- If train is empty, ask Controller for best Target if train.passenger == nil then nextPassenger= Controller:nextPassenger(train) return {['x']= nextPassenger.x, ['y']= nextPassenger.y} -- if train has passenger ask him where to go else return {['x']= train.passenger.destX, ['y']= train.passenger.destY} end end -- ----------------------------------------------------------------------------- Helper functions function distance(startX, startY, destX, destY) return math.sqrt((startX - destX)^2 + (startY - destY)^2) end function maxmin( t ) local max = -math.huge local min = math.huge for k,v in pairs( t ) do if type(v) == 'number' then max = math.max( max, v ) min = math.min( min, v ) end end return max, min end function checkTable(candidate) if type(candidate) == "table" then return 1 else print("Not a table but "..type(candidate)) return 0 end end -- print any table function printTable(table, lvl) if checkTable(table) then lvl = lvl or 0 for k, v in pairs(table) do if type(v) == "table" then print(v) else str = "" for i = 1,lvl do str = str .. "\t" end print(str, k, v) end end end end