[Lua] Table Help

Come in here to talk about your sky-net style world-destroying super bots!

Moderator: Defcon moderators

User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

[Lua] Table Help

Postby Ace Rimmer » Mon Apr 19, 2010 3:38 pm

I've been trying to come up with a decent way to identify and control units via tables, which are really just arrays in Lua (right?).

I think (?) one of the biggest problems I have is not being able to remove a unit from a table when it's destroyed due to the fact you can only use table.remove() to remove based on position and not value. That is, if there are 12 Carriers and one dies, you can't say "ah, that was unitID 35, which is in position 3 of the table Carriers, delete/remove/set as nil that one. At least, several weeks of trying to work that out have led me to belive it.

Currently, when I place my units (ground or sea), I place each one in it's own table so I can avoid using GetAllUnits() or GetAllOwnUnits() every time I want to do something. This seems to work just fine, except when it comes to unit movement. The game crashes after a while (orders start at Defcon 4, keep going, crashes during Defcon 2, sometimes after Defcon 1, sometimes not). My guess is that I'm trying to give orders to units that don't exist anymore because I never worked out a way to remove destroyed units from my tables.

Just to see what I would get, I used this SendChat(CVs[1]:GetLongitude()) (CVs is a table of my carriers), every tick. Something really odd happens; at first it gives me the longitude of Carrier 1, but then when that carrier (I assume) is destroyed during Defcon 3, the longitude changes, as if it's giving me a coordinate of Carrier n, and then soon Lua is throwing an error (which I expected) stating that I'm trying to call a value that is nil. The part I don't get is why the value would change instead of going straight to nil and the error showing up.

It could also be happening because I do call GetAllUnits() every tick in a different function to check for enemy units in radar, scan my enemy units table (one for each type of unit) and insert that unit if it's 'new'. I then use those tables to base my fleet movements off of. E.g., if enemy surface-ship is within x distance of myCarrier, move myCarrier.

Once again, I have very little understanding of what I'm doing, but any advice would nice.
User avatar
martin
level5
level5
Posts: 3210
Joined: Fri Nov 19, 2004 8:37 pm
Location: ::1
Contact:

Postby martin » Mon Apr 19, 2010 6:25 pm

Tables in Lua aren't arrays, they're associative arrays. What this means is that instead of an array where the key has to be an integer, the key can be anything. In lua to remove an item from the table you can do

Code: Select all

table[key] = nil


And then the value is no longer in the table.

Now that's cleared up, I'd suggest organising your table to map from UnitID -> Table. The table returned can store whatever data from like about a particular unit.

Obviously to remove units form the table, simply monitor the death events and do

Code: Select all

unitTable[UnitID] = nil


when one occurs
GENERATION 22:The first time you see this, copy it into your sig on any forum and add 1 to the generation. Social experiment.
User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

Postby Ace Rimmer » Mon Apr 19, 2010 7:21 pm

martin wrote:Now that's cleared up, I'd suggest organizing your table to map from UnitID -> Table. The table returned can store whatever data from like about a particular unit.

Obviously to remove units form the table, simply monitor the death events and do

Code: Select all

unitTable[UnitID] = nil


when one occurs

So, how do you map it that way? (UnitID -> Table) Currently, I just use table.insert(unittypeTable, unit) using a for loop, so the key is 1, 2, etc and value is UnitID, which means I have no way to know which position the unit has in the table.

Also, it's definitely the calling of destroyed units that crashes Defcon. I replaced all table references in ship movement and it works fine. I've even been able to get units to move together (i.e. one carrier moves in response to enemy ship coming into radar, all nearby carriers 'follow'). What I haven't figured out for that bit is how to make them do it in an orderly fashion. Right now, they end up getting all bunched together and/or can't/don't keep good formation lines.
User avatar
martin
level5
level5
Posts: 3210
Joined: Fri Nov 19, 2004 8:37 pm
Location: ::1
Contact:

Postby martin » Mon Apr 19, 2010 8:27 pm

Sorry, I wasn't very clear on how to achieve that was I?

Ok, so when you discover a new node with UnitID ID you do this:

Code: Select all

UnitTable[ID] = something


Something, in this case, is whatever data you want to store for this unit. In this case, what I'd recommend doing is storing a new table as the value, and this table would contain unit specific data. Stuff like unit type, position, some kind of abstract state in your AI algorithm for this unit, whatever you like.

Code: Select all

if (UnitTable[ID] == nil) --determine if this is a new unit

    --create a table for this unit
    local unitTable = {}

    --now put some useful data into unitTable
    --whatever you like really
    unitTable.Position = GetPosition(ID)
    unitTable.Friendly = GetTeam(ID) == MyTeam

    UnitTable[ID] = unitTable
end


Once you've done that, in the future you can do

Code: Select all


--get data
local isUnitFriendly = unitTable[ID].Friendly
local positionAtLastDiscovery = unitTable[ID].Position

--update data
unitTable[ID].Position = GetPosition(ID)

--Add new data at any time
unitTable[ID].Foo = "Bar"

--Delete data at any time
unitTable[ID].Foo = nil

--Delete unit at any time
unitTable[ID] = nil
GENERATION 22:The first time you see this, copy it into your sig on any forum and add 1 to the generation. Social experiment.
User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

Postby Ace Rimmer » Mon Apr 19, 2010 9:06 pm

So, have a master table, made up of tables, each on being the unitID Defcon assigns to each unit:

MyCarriers = {unitID ={stuff/state}, unitID ={stuff/state}, etc} ?

And just delete them as they're destroyed: eventtype = "destroyed", targetID == nil

(targetID actually being a unitID, which is actually a table)

Am I getting this right :?:

Also, ships now move in formation. Whee!
User avatar
martin
level5
level5
Posts: 3210
Joined: Fri Nov 19, 2004 8:37 pm
Location: ::1
Contact:

Postby martin » Mon Apr 19, 2010 9:43 pm

You have the concept right yeah, except that to remove the unit when it dies it's

Code: Select all

unitTable[targetID] = nil


NOT

Code: Select all

TargetID = nil
GENERATION 22:The first time you see this, copy it into your sig on any forum and add 1 to the generation. Social experiment.
User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

Postby Ace Rimmer » Mon Apr 19, 2010 9:45 pm

Well, I meant in my example, MyCarriers[targetID] = nil. :?:
Smoke me a kipper, I'll be back for breakfast...
User avatar
martin
level5
level5
Posts: 3210
Joined: Fri Nov 19, 2004 8:37 pm
Location: ::1
Contact:

Postby martin » Mon Apr 19, 2010 10:39 pm

Assuming that you know the unit you're killing is a carrier.

In Joshua I was using a single global entity table for units because any API calls seem to be incredibly slow. So instead of GetUnitType(UnitID) I sped it up vastly by doing that just once when the unit is first encountered, and then I can just look it up in the table. My advice would be to do this, have a single central table of units with *all* unchanging data cached.

If at some point you want a table of units of just one type, then build a new one from your global table for a single use:

Code: Select all

UnitTable --Contains all units

GetTableFilteredByType(type)
    local result = {}

    for (k, v in pairs(UnitTable))
        if (v.Type = type) then
            result[k] = v
        end
    end

    return result --result contains all units of a specific type
end


This is a really fast operation, so you can do it quite a lot.

This way, all your units are in one place and one place only. So you won't have trouble with dead units hanging around in the table, but you can still access things by type. Obviously you can build filter methods for any number of different filters. If you modified the filter methods to accept a table, rather than use the global entity table you could even chain filters together which is cool.
User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

Postby Ace Rimmer » Fri May 07, 2010 8:26 pm

Question:

Based on your example above, I created the code below. However, the OnEvent bit doesn't seem to work. The best I've been able to do is get it to show me 'nil' for all values in UnitTable. What exactly am I doing wrong here? If it's unclear, what I'm trying to do is set the 'Condition' of any unit that has been destroyed from "Active" to "Killed".

I know the targetID in the OnEvent function returns the ID of the unit that was killed. That same ID should also be the key in UnitTable, right? Therefore UnitTable[targetID] should equal UnitTable[someID] (vs nil).

Code: Select all

UnitTable = {}
   
function RadarSweep()
   local units = GetAllUnits ()
   for i, unit in ipairs(units) do
      if UnitTable[unit] == nil then --create a table for this unit
         local unitTable = {}
         --user defined variables
         unitTable.Condition = "Active" --vs "Killed" or "Invalid" (alive but not visible)
         unitTable.Mode = "Unknown" --[[vs Attack, Defend, <snip>]]
         --game defined variables
         unitTable.Team = unit:GetTeamID() --etc, etc
         
         UnitTable[unit] = unitTable
      end   
   end
end

function OnEvent(eventType, sourceID, targetID, unitType, longitude, latitude)
   if (eventType == "Destroyed") then
      UnitTable[targetID].Condition = "Killed"
   end
end
User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

Postby Ace Rimmer » Mon May 10, 2010 4:20 pm

Really, nobody knows why UnitTable ends up with zero entries?
Smoke me a kipper, I'll be back for breakfast...
User avatar
Montyphy
level5
level5
Posts: 6745
Joined: Tue Apr 19, 2005 2:28 pm
Location: London, England

Postby Montyphy » Mon May 10, 2010 4:54 pm

I strongly encourage you to NEVER use two variables with the same name but differentiated by case, especially when the upper and lower case versions of a character don't look very different from each other. You should probably also use a naming convention which makes it easy to spot the difference between a function and a variable, something like, variables lower cased and underscore_separated, functions CamelCased, and attributes or keys lower case.

Also, when and how is RadarSweep called?
Uplink help: Check out the Guide or FAQ.
Latest Uplink patch is v1.55.
User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

Postby Ace Rimmer » Thu May 13, 2010 5:24 pm

RadarSweep is called every tick.

Alright, I 'fixed' the naming a bit and discovered the reason I was having trouble; I didn't (and still don't) fully understand how tables work, but apparently asking it to tell me how many values were in MainUnitTable was silly for two reasons...

1. All keys are light userdata and Lua was looking for a start (i.e. numeric 1 or it's equivalent) and didn't see it.
2. All keys are also tables.

I guess. Anyway, this is what I've gotten so far:

Image

As you can see, I am now accessing the MainUnitTable (formerly UnitTable, not to be confused with unitTable) just fine, but the onEvent = "Destroyed" doesn't quite work, er, all the time. You can clearly see it working, but I can't figure out why it would say line 109 is a nil value. unitID <93> is most certainly there, as the chat shows.

Could it be because on some occasions multiple gunshots hit the same target and more than one unit (e.g. enemy BattleShip, enemy Bomber, enemy Fighter) is getting credit as sourceID and therefore the onEvent is trying multiple times to change the value of MainUnitTable[targetID].Condition?

In short, why does it work some of the time? /me is confused

Code: Select all

104 function OnEvent(eventType, sourceID, targetID, unitType, longitude, latitude)
105   if (eventType == "CeasedFire") then
106   elseif (eventType == "Destroyed") then
107      SendChat(" ")
108      SendChat(tostring(targetID) .. " Change Condition")
109      MainUnitTable[targetID].Condition = "Killed" --change the condition from "Active" to "Killed"
110      SendChat(tostring(targetID) .. MainUnitTable[targetID].Condition) --make sure it actually changed
111      SendChat(tostring(targetID) .. "Condition Changed")
112      SendChat(" ")   
113   elseif <etc, etc, blah>
127   end
128 end
User avatar
Montyphy
level5
level5
Posts: 6745
Joined: Tue Apr 19, 2005 2:28 pm
Location: London, England

Postby Montyphy » Thu May 13, 2010 5:55 pm

Ace Rimmer wrote:unitID <93> is most certainly there, as the chat shows.

Code: Select all

108      SendChat(tostring(targetID) .. " Change Condition")
109      MainUnitTable[targetID].Condition = "Killed" --change the condition from "Active" to "Killed"
110      SendChat(tostring(targetID) .. MainUnitTable[targetID].Condition) --make sure it actually changed
111      SendChat(tostring(targetID) .. "Condition Changed")


The chat shows that targetID is being assigned the value 93 when passed to OnEvent but the error shows that no unit with that ID is in your table. Only had a quick glance but I think I would need to see more of the overall code, in particular how and when RadarSweep is called in relation to OnEvent, before being able to tell why that unit isn't being added (or prematurely removed) from your MainUnitTable. At what point do you remove units from that table?
Uplink help: Check out the Guide or FAQ.

Latest Uplink patch is v1.55.
User avatar
Ace Rimmer
level5
level5
Posts: 10803
Joined: Thu Dec 07, 2006 9:46 pm
Location: The Multiverse

Postby Ace Rimmer » Thu May 13, 2010 6:05 pm

Code: Select all

function OnTick()

   if (DefconLevel ~= GetDefconLevel()) then
      DefconLevel = GetDefconLevel()
      if (DefconLevel == 5) then
         --CircleTest()
         OnFirstTickD5()
         --WhiteboardDraw(-180, 0, 180, 0)
         --WhiteboardDraw(0, -90, 0, 90)
      elseif (DefconLevel == 4) then
         OnFirstTickD4()
      elseif (DefconLevel == 3) then
         OnFirstTickD3()
      elseif (DefconLevel == 2) then
         OnFirstTickD2()
      elseif (DefconLevel == 1) then
         OnFirstTickD1()
      end
   end
   
   if (DefconLevel == 5) then
      TickD5()
   elseif (DefconLevel == 4) then
      TickD4()
   elseif (DefconLevel == 3) then
      TickD3()
   elseif (DefconLevel == 2) then
      TickD2()
   elseif (DefconLevel == 1) then
      TickD1()
   end
   WorkOnLongTasks()
end


Code: Select all

function TickD5()
      RadarSweep()
end


Every function TickDn RadarSweep is called

Code: Select all

function RadarSweep()

   local units = GetAllUnits ()   
   for i, unit in pairs(units) do
      if MainUnitTable[unit] == nil then --create a table for this unit
         local unitTable = {}
         
         --user defined variables
         unitTable.Condition = "Active" --vs "Killed" or "Invalid" (alive but not visible)
         unitTable.Mode = "Unknown" --[[vs Attack, Defend, Scout, Patrol,
                                 NukeWhales (naval nuking),
                                 IBeam (bomber formation vs city), IBeamLeader,
                                 Idle, Leader, Follower]]
         --game defined variables
         unitTable.Team = unit:GetTeamID()
         unitTable.Type = unit:GetUnitType()         
         unitTable.Long = unit:GetLongitude()
         unitTable.Lat = unit:GetLatitude()
         unitTable.Speed = unit:GetVelocity()
         unitTable.FirstSeen = GetGameTime()
         
         local us = GetOwnTeamID()
         if unitTable.Team == us then
            unitTable.State = unit:GetCurrentState()                  
            unitTable.Orders = unit:GetActionQueue()
            unitTable.Timer = unit:GetStateTimer()
            if (unitTable.Type == "Carrier" or unitTable.Type == "AirBase" or unitTable.Type == "Bomber" or unitTable.Type == "Silo") then
               unitTable.Target = unit:GetCurrentTargetID()
               unitTable.Nukes = unit:GetNukeCount()
            end
         end   
         MainUnitTable[unit] = unitTable
         SendChat(tostring(unit) .. " " .. MainUnitTable[unit].Type .. " " .. MainUnitTable[unit].Condition)

      end         
   end


I don't plan on removing any units (none are removed now). Other than placement, no other code is active.
User avatar
Montyphy
level5
level5
Posts: 6745
Joined: Tue Apr 19, 2005 2:28 pm
Location: London, England

Postby Montyphy » Thu May 13, 2010 6:18 pm

What does the chat say if you change:

Code: Select all

108      SendChat(tostring(targetID) .. " Change Condition")


to be:

Code: Select all

SendChat(tostring(targetID) .. " : " .. tostring(unitType)  .. " : Change Condition")


Just curious if it's the same unit type that causes the problem i.e. ground installations or fighter. Silly question (as I haven't used the API much) but does GetAllUnits() return placed structures as well as planes and fleets?
Uplink help: Check out the Guide or FAQ.

Latest Uplink patch is v1.55.

Return to “AI Bots”

Who is online

Users browsing this forum: No registered users and 2 guests