Log In  


Recently, I've been working on a word-typing game called Catreeboard with my son. At the beginning and to simply get the game to a state where we could work on animations and gameplay, I naively created a simple table-based dictionary that is subdivided by length of the word. It looked something like this:

dictionary = {
    {
        "a",
        "i",
        "on",
        "at",
        "it",
        "is",
    },
    {
        "cat",
        "dog",
        "sun",
        "hat",
        "bat",
        "pen",
    },
    {
        "home",
        "love",
        "ball",
        "star",
        "blue",
    },
    {
        "house",
        "quick",
        "plant",
        "water",
        "space",
    }, {
        "family",
        "person",
        "animal",
        "window",
        "garden",
    },
    {
        "picture",
        "balance",
        "teacher",
        "history",
        "mountain",
    }
}

Whenever we wanted a new word, we could get one pretty easily with the following expression:

local word = rnd(dictionary[mid(1, game_level, #dictionary)])

> Notice the mid() call here to clamp our selection to what is actually available in the dictionary, this way the game level can continue to grow but not go out of range of our dictionary levels.

This was a simple way to get 6 levels worth of words to test with, but now we're at the point where we're ready to expand the dictionary to have more variety. In its current form, every word we added would also be adding a token. We wanted a dictionary of at least 50 words per level with potentially even further levels that would also have 50 variations each. This can add up to a ton of tokens very quickly.

Taking a look at the PICO-8 manual to see if there was something we could use to make this more efficient, this passage came to light:

> The number of code tokens is shown at the bottom right. One program can have a maximum of 8192 tokens. Each token is a word (e.g. variable name) or operator. Pairs of brackets, and strings each count as 1 token. commas, periods, LOCALs, semi-colons, ENDs, and comments are not counted.

Combining all the words per level into a single string, we could reduce the tokens to just 6 tokens for all the levels. Our dictionary thus became something like this:

dictionary = {
        "a,i,on,at,it",
        "cat,dog,sun,hat,bat",
        "home,love,ball,star,blue",
        "house,quick,plant,water,space",
        "family,person,animal,window,garden",
        "picture,balance,teacher,history,mountain",
}

We now need to do something in order to get a single word. To keep the simplicity of what we're doing to pick a word, we can use a Lua feature called "metatables". Using metatables, we can intercept a call to an index in the table like dictionary[level] and call a custom function to parse and return the structure that we had before. The first change we need to do is not expose this table with strings directly, so we'll rename dictionary to local packed_dictionary. Next we'll create a metatable function that will read from this and respond with results like before:

local packed_dictionary = {
        "a,i,on,at,it",
        "cat,dog,sun,hat,bat",
        "home,love,ball,star,blue",
        "house,quick,plant,water,space",
        "family,person,animal,window,garden",
        "picture,balance,teacher,history,mountain",
}

local dictionary_access = {
    __index = function (table, key)
        return split(packed_dictionary[key])
    end,
}

dictionary = setmetatable({}, dictionary_access)

Now if I access dictionary[1], it will give me:

{
    "a",
    "i",
    "on",
    "at",
    "it",
    "is",
}

However, each time we access dictionary[1] it's going to call our __index function. This is because there is not entry for dictionary at the key of 1. Let's use this as an opportunity to "lazy load" and assign it to the dictionary so it doesn't have to constantly parse:

local dictionary_access = {
    __index = function (table, key)
        -- table in this instance is `dictionary`
        table[key] = split(packed_dictionary[key])
        return table[key]
    end,
}

Finally, we need to be able to get the length of the dictionary in order to be able to do the level selection we were doing before. We can use the __len metatable key to accomplish this:

local dictionary_access = {
    __index = function (table, key)
        table[key] = split(packed_dictionary[key])
        return table[key]
    end,
    __len = function (table)
        return #packed_dictionary
    end,
}

With this in place, we can expand the dictionary of each level without increasing tokens, and any additional "levels" of words we want to add to the dictionary would cost only a single token. We'll just have to keep an eye on the character count.

This snippet of code is being used in Catreeboard currently, however here is a simple cartridge to see it in action without the context of a game around it:

Cart #zidiyuhuku-1 | 2024-08-29 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
1

1


You can have the entire dictionary as a single string with two different separators and expand it to regular 2D array with a little loop in _init
Overall token cost would be fixed regardless of dictionary size.
Split has abn optional parameter to specify a different separator, coma is just the default


@RealShadowCaster Agree, if I get limited on tokens again, I'd do another compression as you suggested. The main thrust of this was to eliminate the individual tokens per word.


1
dictionary={}
foreach(
split([[a,i,on,at,it,is
cat,dog,sun,hat,bat,pen
home,love,ball,star,blue
house,quick,plant,water,space
family,person,animal,window,garden
picture,balance,teacher,history,mountain]]
,"\n"),function (s) add(dictionary,split(s)) end)

fixed 18 tokens for the whole dictionary
You also gain the " and {} delimiters, in character count.



[Please log in to post a comment]