Creating multiple highscores?

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
Plu
Inner party member
Posts: 722
Joined: Fri Mar 15, 2013 9:36 pm

Re: Creating multiple highscores?

Post by Plu » Tue Aug 12, 2014 8:39 am

How does the table know what the position and score vars are? We never defined them?
It uses the magic of ipairs. ipairs is a function that loops over a table, and returns for each row in the table it's (integer) key and it's current value. By putting that in a for loop, we can catch these values and use them for calculations.

So if you have this table:

Code: Select all

t = { 6, 9, 12, 15 }
And you run this loop:

Code: Select all

for key, value in ipairs( t ) do
  -- code here
end
Then the part of code here will be called 4 times, and the content of the key and value variables will change for each loop, to be (in order:)
key = 1, value = 6
key = 2, value = 9
key = 3, value = 12
key = 4, value = 15

As to why it only uses one score slot; I'm not sure whether that's a problem with the drawing or with the table you're using. Can you use this highscores table and see what it does?

Code: Select all

highscores = { 500, 400, 100, 10 }
Also, it helps if you post a .love file of your current progress, it might help us see what you're doing :)

User avatar
Roland_Yonaba
Inner party member
Posts: 1563
Joined: Tue Jun 21, 2011 6:08 pm
Location: Ouagadougou (Burkina Faso)
Contact:

Re: Creating multiple highscores?

Post by Roland_Yonaba » Tue Aug 12, 2014 6:17 pm

Hi Alex,

Sit tight, grab some coffee, and brace yourself, for this is about to be a quite lengthy post.
It might be a bit off-topic, but will indeed teach you one of things, and hopefully you will find the rest on your own.
Please, bear with me, as english is not my primary language; in case anything is unclear, let me know, so that I can provide more details.

I will discuss tables in general, then some subleties/gotchas about the way Lua handles tables. Then, I will come back on how you can maintain a list of highscores and perform some various common operations on this list of scores. Then I will discuss briefly about serialization.

Let us proceed.

Tables
In programming, in general, the term "table" always refer to the same thing : "a data structure". Or simply said, a nice way to organize information. Tables are very, very, very handy, just because they let you group, put together pieces of information or data that belong together, or just keep them together and easily process them like items on a list.

If I am holding a store, and I want people to easily find what they are looking for, I will show them a list of things I sell in my store. Better, I will sort that list of items alphabetically. This list is indeed "a table". If I am writing a game where there would be dozens of ennemies attacking the player, I will list those baddies in a table. The advantage of having such a data structure is I can keep all those entities tight together at the same place, and then I can easily loop through the collection and apply a specific action on some specific entities in a very straightforward and elegant manner.

In Lua, there are two types of tables. You have "arrays" and "associative arrays", also called "hash tables".
Arrays are tables where indices start at 1, and are only numeric. Only numeric. And consecutive. That is the whole point.
The following is a table, more precisely an array, containing three elements. The first element is "a" (index is 1), second is "b" (index is 2), third is "c" (index is 3).

Code: Select all

t = {"a", "b", "c"}
A very common operation one can do on "arrays" is iteration: looping through the entire collection. But most importantly, in order. The traversal order of an array is predictable, because you know for sure that the first index will be 1, and that indices are consecutive integers.So assuming t is an array, you can proceed as follows to print all the contents of the array.

Code: Select all

for index = 1, #t do
  local value = t[index]
  print(index, value)
end
Here, I have used then length operator "#" which returns the size of an array (i.e, the number of elements it contains).
This way of doing iteration is called "numeric for loop".

An equivalent method would be using Lua's dedicated function, ipairs.

Code: Select all

for index, value in ipairs(t) do
  print(index, value)
end
Of course, arrays can have holes, but generally you do not want such thing to happen.
Also, you can have indices which are less than 1. But that won't make it behave like an array anymore.

Code: Select all

t = {1,2,3,4,5} -- an array of 5 elements
t[4] = nil -- creates a hole
t[0] = 0
t[-1] = -1
This is still a valid table, but not a convenient array. Because ipairs, for example, will stop yielding values at the hole, and will not start iterating at index -1, nor at index 0, as the first expected index is assumed to be 1.
Of course, there are ways to handle such tables to retrieve all elements (using pairs, or next), but for simple use cases, you won't have to deal with such arrays.

"Associative arrays" are a bit more complex than arrays, but they are actually the reason for what Lua's is beautiful, and quite powerful (IMHO). "Associative arrays" are basically tables, in the sense that they hold a set of elements (values) together, but those elements are not stored in any particular order. Instead, indices are not (consecutive) integers, starting from 1, but they can be any Lua first-class value : a decimal number, an integer, a string, a function, a table, a coroutine, anything.
In most of use cases, people will use strings to index values in "associative arrays". For instance, If I want to represent a player entity in my game, I need to keep track of his position on the screen, a pair of x,y coordinates. I also want to track down his health, his money, etc. I would use then an "associative array":

Code: Select all

player = { x = 10, y = 20, health = 50, money = 0}
Here, the table player contains 4 values. They are indexed with strings : "x", "y", "health" and "money". I can retrieve any of these values using the "dot notation".

Code: Select all

print(player.x) --> 10
print(player.y) --> 20
print(player.health) --> 50
print(player.money) --> 0 (poor dude!)
Note that here, there is no particular order for all those 4 values: noone can say if money comes before health, or x coordinate should come before y. In "associative array", there is no particular order, as I was saying before. Therefore, I cannot use the numeric for loop, nor ipairs, because all those assume that there is an order defined. Therefore, for associative arrays, I will use something more general. Conveniently, Lua's provide a function for that, named pairs.

Code: Select all

for key, value in pairs(player) do
  print(key, value)
end
This will print out the contents of the "associative array" player, but not in a predictable order. But it will definitely process all the elements inside that "associative array".

Side note, pairs also does what ipairs does, as it is much more general. So yes, using pairs on a simple array will work, too. But ipairs cannot do what pairs does, so using ipairs on an "associative array" will produce nothing. :)
Generally, Lua users tend to use pairs when they are uncertain of the contents of a table. If one is not sure a table is an array, one will just use pairs to be sure to get all the elements inside. And in case I am definitely sure a table is an array, I will just use ipairs, or a numeric for loop.

But arrays and associative arrays can be mixed. Yes.

Code: Select all

t = {x = "x", y = "y", 1, z = "z", 2, 3}.
What Lua does here is creating an associative array that will have two parts: a hash part, and an array part.
Using ipairs or numeric for will only process the array part. you will get pairs (1,1), (2,2) and (3,3).
Using pairs will process the whole contents of the table, in no particular order though.

And i'll stop here for tables. I will leave some links for complementary details I left out, and I will strongly recommend to read them later. My point is, Lua's primary data structure are tables. So once you understand them, and how powerful they are, you can do lots of things on your own with the language. That won't be time wasted.
Now we want highscores.

You want to maintain a list of highscores. I will outline here a simple method to handle it. I won't strive here for efficiency, but for simplicity and effectiveness.
Let us start with an empty array.

Code: Select all

highscores = {}
I would like to add a new score. Since the array is empty, I will just have to insert it.

Code: Select all

table.insert(highscores, newscore)
Quite simple. But let' say I also want to keep track of a maximum number of scores n.
In other words, I don't want the array to contain more than n elements. Then, if I have a new score I have to check first if this new score is worth being added: if yes, I will insert it and remove the lowest score. If not, the new score will just be dropped.

Let us write a routine (a function, more precisely) that will handle this for us gracefully.
Let us assume the table is already sorted in decreasing order : the higher scores come first, the lowest ones come last.

Code: Select all

-- highscores is the array of scores, newscore is the score to be added and n is the max number of scores
-- to keep track of.
function addscore(highscores, newscore, n)
  -- get the lowest score, at the end of the list
  local lowestScore = highscores[n]
  if newscore > lowestScore then
    table.remove(highscores, n) -- the size of the table becomes now n-1
    table.insert(highscores, newscore) -- adds at the end of the actual table the new score.
    table.sort(highscores, function(a,b) return a>b end) -- sort the table in decreasing order
  end
end
The above function can be implemented in a different way. No need to remove and add again a new entry in the array; you can just assign the new value to the last index. Simpler.

Code: Select all

function addscore(highscores, newscore, n)
  -- get the lowest score, at the end of the list
  local lowestScore = highscores[n]
  if newscore > lowestScore then
    highscores[n] = newscore
    table.sort(highscores, function(a,b) return a>b end) -- sort the table in decreasing order
  end
end
Also, you can handle the table in a different manner : instead of keeping scores in decreasing order, you can maintain it in increasing order. The lowest scores come first, and the higher ones come last. In that way, to add a new score, you will have to compare it to the first entry (at index 1). If it is higher, then it is worth being added.

Code: Select all

function addscore(highscores, newscore, n)
  -- get the lowest score, at the end of the list
  local lowestScore = highscores[1]
  if newscore > lowestScore then
    highscores[1] = newscore
    table.sort(highscores) -- sort the table in increasing order
    -- table.sort(highscores, function (a,b) return a<b end) would do exactly the same than just "table.sort(highscore)".
  end
end
In case you want to print the list of scores, since the table is an array, just use ipairs or a numeric for to loop through values and print them.
Note that, in case highscores are listed in increasing order, you will have to loop from the last index to the first one to get the higher values first. Like this:

Code: Select all

highscores = {100,200,300,400}
for i = #highscores, 1, -1 do -- numeric for, iterating backwards
  print(highscores[i]) -- will print 400, 300, 200, then 100.
end
This is quite easier to get as soon as you are familiar with the basic operations on tables. You might even come up with something nicer on your own, and modify it in case you want to keep track of best scores associated with names: you just need to handle an array of associative arrays. This is getting fancier. :crazy:

Serializing ?
Last, thing, serialization. Well, the point here is, you have a table in memory. And you want to write it down in a file so that you can read it and restore it back in memory again as it was. Since that array (and its contents) are data, you will have to turn it it into a format that you can write down (readable or not) and in a way such you can get the exact same information without any losses. This is called serialization.

Yes indeed, there are lots of serialization libraries around, you can get them and use them (see the wiki), but you can also try to write some serialization function for simple use cases: arrays, associative array with string keys, etc. See How can I dump out a table ? (Luafaq). Libraries are relevant in the case the table you want to serialize contains fancy things, such a cyclic references, metatables associated to the table, keys that are functions, userdata, or tables themselves...

Hope all of this will help.

User avatar
AlexCalv
Prole
Posts: 42
Joined: Fri Aug 08, 2014 7:12 pm

Re: Creating multiple highscores?

Post by AlexCalv » Tue Aug 12, 2014 8:57 pm

Maybe programming isn't my thing. This frustrates me so much because I try to learn and want to but I can't. I feel like reading that over 3-4 times helped me a little bit to understand tables better but I still don't understand how to make this work. The highscores don't work at all now. It just constantly prints 0 to the console when I'm looking at the highscores page.

This code gives me an error when I try to use the function once the player lost.

Code: Select all

(main:line:392) function addScore(highscores, newScore, n)
	local lowestScore = highscores[n]
	newScore = player.score
	if newScore > lowestScore then
		table.remove(highscores, n)
		table.insert(highscores, newScore)
		table.sort(highscores)
	end
end
This is where I'm putting the function

Code: Select all

(player:line:57) function player.Checks(dt)
--Only saves if keyboard is enabled
	if player.lives <= 0 then
		gamestate = "lost"
		addScore(highscores, newScore, n)
		if keyboard == 1 then
			endgameSave()
		end
	end
Attachments
Collector.love
(185.08 KiB) Downloaded 38 times

Post Reply

Who is online

Users browsing this forum: No registered users and 32 guests