Paul
Paul

Reputation: 1327

Effectively remove nil values from lua table

I am new to lua and fairly new to programming. I am trying to achieve the following.

I have a table of numbers from which I pick a random number.

myTable = {}

for i = 1 to 100 do 
   table.insert(myTable, i)
end

local numberChosen = myTable[math.random(#myTable)]

So far, so good. The next time a number is picked, I want that number to be removed from the table. I know lua doesn't remove values, they stay there as nil. So

table.remove(myTable, numberChosen)

Doesn't work as when I try to run the random function again, if the value is nil, I get "bad argument #1 to 'random' (interval is empty)"

I have tried creating a function like this:

function cleanTable(t)
    local  cleanTable = {}
    for k, v in ipairs(t)do
        if v ~= nil then
            table.insert(cleanTable, v)
        end
    end
    return cleanTable
end

myTable = cleanTable(myTable)

But that does't work either as the random function returns the same error. Can anyone help?

edit ----------------------------------------------------------------------

My table contains keys and values like so:

local columns = {1,2,3,4,5,6,7,8,9,10,11,12}
local rows = {1,2,3,4,5,6,7,8,9,10,11,12}
local coupledNumbers = {}


for i = 1, #columns do
    for a = 1, #rows do
        local coupledNumber = i * a
        table.insert(coupledNumbers, i*a, coupledNumber)
    end
end

How do I remove the key and the value? thanks

Upvotes: 2

Views: 1871

Answers (4)

Oleg V. Volkov
Oleg V. Volkov

Reputation: 22421

I know lua doesn't remove values, they stay there as nil.

No, that's not how it works. That's how it works according to docs:

The table.remove function removes (and returns) an element from a given position in an array, moving down other elements to close space and decrementing the size of the array.

-- https://www.lua.org/pil/19.2.html.

Here's your problem:

local randomPosition = math.random(#myTable) -- get random POSITION
local numberChosen = myTable[randomPosition] -- get VALUE from that POSITION
table.remove(myTable, numberChosen)          -- try to remove element, but instead of specifying its position as .remove documentation demands you specify some arbitrary value you've found at that position.

Your logic works for the first time purely by coincidence, only because you just happen to have an array with values matching its positions.

You need to remove by position instead: table.remove(myTable, randomPosition).

Your second function is not only useless, it makes no sense. for in ipairs will never return nil value - it's end of array, so for loop will just close.

Upvotes: 0

csaar
csaar

Reputation: 560

You can use a key-value-table instead of a numeric table. Code is a bit ugly.

local columns = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
local rows = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
local coupledNumbers = {}

-- same like `i = 1, 12` - you don't even need the tables `columns` and `rows`, 'cause you aren't indexing
for i = 1, #columns do
  for j = 1, #rows do
    local idx = i * j
    if not coupledNumbers[idx] then
      coupledNumbers[idx] = true
    end
  end
end

local function getRand(n)
  local res
  if next(coupledNumbers) ~= nil then
    repeat
      local r = math.random(n)
      if coupledNumbers[r] then
        res, coupledNumbers[r] = r, nil
      end
    until res
  end

  return res
end

print(getRand(144))
print(getRand(144))
print(getRand(144))
print(getRand(144))
print('---')

for i in pairs(coupledNumbers) do
 print(i)
end

Upvotes: 0

Nifim
Nifim

Reputation: 5031

You are improperly defining numberChosen. when you define

local numberChosen = myTable[math.random(#myTable)]

You are mistakenly defining numberChosen to the value at some arbitrary index in the table.

Let us step through a few loops of your code

Loop: 1     numberChosen: 1     #myTable 100
Loop: 2     numberChosen: 57    #myTable 99
Loop: 3     numberChosen: 20    #myTable 98
Loop: 4     numberChosen: 82    #myTable 97
Loop: 5     numberChosen: 60    #myTable 96
Loop: 6     numberChosen: 48    #myTable 95
Loop: 7     numberChosen: 35    #myTable 94
Loop: 8     numberChosen: 91    #myTable 93
Loop: 9     numberChosen: 82    #myTable 92
Loop: 10    numberChosen: 74    #myTable 91
Loop: 11    numberChosen: 17    #myTable 90
Loop: 12    numberChosen: 86    #myTable 89
Loop: 13    numberChosen: 70    #myTable 88
Loop: 14    numberChosen: 49    #myTable 87
Loop: 15    numberChosen: 30    #myTable 86
Loop: 16    numberChosen: 3     #myTable 85
Loop: 17    numberChosen: 10    #myTable 84
Loop: 18    numberChosen: 38    #myTable 83
Loop: 19    numberChosen: 16    #myTable 82
Loop: 20    numberChosen: 17    #myTable 81
Loop: 21    numberChosen: 100   #myTable 80 

Look what happens here on loop 21 we have number 100, an index which can not exist. We have removed 20 items from the list so the list is only 80 items long.

when we call math.random(#myTable) we confine the result to be with in the bounds of our table, but when we do myTable[math.random(#myTable)] we no longer have that confidence.

As we removed values from our table it shrinks, but the values inside the table do not change, resulting in an increase of values which reference indices outside bounds of our table.

So we need to define numberChosen not as myTable[math.random(#myTable)] but as math.random(#myTable), also to insure we have unique index values i have adjusted the for loop for building the array:

local columns = {1,2,3,4,5,6,7,8,9,10,11,12}
local rows = {1,2,3,4,5,6,7,8,9,10,11,12}
local coupledNumbers = {}

print(coupledNumbers == {})


for i = 1, #columns do
    for a = 1, #rows do
        local index = ((i - 1) * #rows) + a
        coupledNumbers[index] = index
    end
end

for i = 1, 144 do
  local numberChosen = math.random(#coupledNumbers)
  print(i, numberChosen, coupledNumbers[numberChosen])
  table.remove(coupledNumbers, numberChosen)
end

Upvotes: 1

Taazar
Taazar

Reputation: 1505

The way I would prefer to tackle this is to generate a board (a 2D array) and set each element to true when it is hit. You then just check the board when each hit is chosen and if it's already true then it's a re-hit, if it's false that is a new hit.

Here is the array at the very start:

StartArray

And here is it at the end of 100 random hits:

EndArray

There aren't 100 trues because quite a few are re'hits which you can see from the console below:

ConsoleLog

You can change the board size from boardHeight and boardWidth in the playGame function and if you need any help integrating this leave a comment and I'll help you out.

local function getNewBoard(newHeight, newWidth)
    local newBoard = {}
    for i = 1, newHeight do
        newBoard[i] = {}
        for j = 1, newWidth do
            newBoard[i][j] = false
        end
    end
    return newBoard
end

local function playGame()
    local boardHeight = 10 --Change to your height
    local boardWidth = 10 --Change to your width
    local gameBoard = getNewBoard(boardHeight, boardWidth)

    for _ = 1, 100 do
        --Get a random hit, you can change this to player entered
        local hitHeight = math.random(1, boardHeight)
        local hitWidth = math.random(1, boardWidth)

        if gameBoard[hitHeight][hitWidth] == true then --Check if already hit
            print("[" .. hitHeight .. ":" .. hitWidth .. "] has already been hit")
        else
            gameBoard[hitHeight][hitWidth] = true --Mark as hit
            print("[" .. hitHeight .. ":" .. hitWidth .. "] has just been hit")
        end
    end
end

playGame()

Upvotes: 0

Related Questions