There are lots of ways to store data. In the Blackjack game, you stored the building blocks for a deck of playing cards in two tables—one for suits and one for values. That’s a good method for small data sets that don’t change from game to game, but it won’t work if, for instance, it was possible for a player to level up and earn the ability to play with Jokers in the deck, because the tables defining the deck is hard-coded into the application.
To make permanent changes to a game environment, or to track player progress, scores, or preferences, you must create a data file outside of the .love file on your user’s computer. Any game that keeps track of a player’s progress has to do this, so it’s a common task, but it does require additional Lua libraries designed to read and write data files.
Bundle the library with your game.
Tell your users to install the libraries before trying to play your game.
The first option is most common in the game industry, but sometimes a library’s license doesn’t allow you to distribute it along with your own application. There’s a strong culture of open source around Lua, so most Lua libraries are licensed to permit you to use them as you please as long as you credit their authors.
Some Lua libraries, however, depend on other applications running on a system, so they must be built especially for those systems. If you use advanced libraries, you have to maintain different builds—one for each platform. Usually, that means one build for Linux, one for Windows, and one for the Mac (unfortunately, Macs are hardware dependent, so you must have a recent Mac available, upon which you can build your release.)
Note
Some game developers choose not to bundle libraries to ensure that their users are free to manage which libraries are on their computer. While most users don’t care about which obscure programming library is on their computers, they probably do care about getting security updates. A library “hidden away” in your .love file isn’t updated along with the rest of a system. So when you do distribute a library, you owe it to your users to check in often with those libraries for important bug fixes and security updates, and then update your own application with the new versions.
Building libraries for each operating system you want your game to run on is an advanced topic outside the scope of this book. There are several good tools, such as win-builds.org, to help you, but this does require advanced knowledge of compiling software. For this reason, this book uses pure Lua libraries that can be bundled with your game and run on any platform with LÖVE installed.
Installing New Libraries
In most games, the kind of data needing storage is not very complex, so usually a simple configuration text file is sufficient. For Lua to know what to do with a text file, the text must have a predictable structure. Highly structured text storage forms a non-relational database that Lua loads into memory and uses just like any other variable you might create in Lua.
There are many popular formats for these kinds of files, including YAML, JSON, and INI. These formats allow you to store data in a consistent structure, which enables its parent application to accurately parse it.
One library (sometimes also called a module) that enables plain text configuration files is inifile . As its name suggests, it interacts with INI configuration files (if you don’t know what that is, you’ll write one soon, so don’t worry).
The best place to find libraries for Lua is luarocks.org, a website dedicated to tracking and distributing Lua libraries. The site is useful for a new Lua programmer because it has several methods for you to search for libraries that you may not even know exist. As you become more familiar with programming, you’ll get a feel for what to expect from any language. The luarocks command will prove far more efficient.
Once luarocks is installed, type it into a terminal to see a helpful message.
The --tree option tells luarocks to create a new folder, called local in this example, for all the files that would normally get installed. With this simple trick, you install all the dependency code you want to use in your project into the project directory itself. Your user doesn’t have to worry about installing anything extra, because it’s all contained in your project.
Now you know why you might want to add a library to a project and how to do it.
Now it’s time to try some libraries to help with configuration files.
Configuration Files
To see how to interact with a text-based configuration file, open Geany and create a new file and enter the following sample data in INI format.
Save the file as sample.ini into your home directory, not the config directory. After all, saving the configuration file into your LÖVE project directory is exactly what you’re trying to avoid, because you want the configuration file to be separate from your application.
Imagine that this file is a save file for a game, with the progress of each player in each configuration block. Were this a real game, you would save a configuration file in a hidden folder named ~/., but for now, you can keep this sample unhidden.
This simple application detects the user’s home directory, detects how the operating system finds its way to the home directory, parses the INI file, and then increments the level entry for player1 by 1.
This tells you that Lua attempted to use the inifile library, but couldn’t locate it because the library isn’t installed on your system; it’s installed in your project directory.
Setting the Package Path
When you created your own card dealer class for Blackjack, you used the require keyword to include your library with your main code. You must do the same for the inifile library.
Just as you generally know where you keep your files on your computer, Lua knows where libraries are normally kept on whatever system it’s installed on. It keeps track of this information in a variable called package.path. If you tell Lua to require a package called foo, then Lua looks in all the locations listed in package.path. When it finds foo, it stops looking and proceeds to execute code. If foo is nowhere to be found, then it throws an error and the application crashes.
If you are adding a library to Lua (or a Lua-based application like LÖVE) that is outside the normal Lua package.path , then you must tell Lua where to look. If you don’t, your program will crash because Lua can’t find a library that you have told it to require.
When require is used in these examples, Lua first searches the current directory for anything ending in .lua. If nothing applicable is found, Lua knows to search /usr/share/lua/5.3 and then /usr/share/lua/5.3/? (Lua itself substitutes ? with the name of the library you provide in require statements).
You can append entries to package.path in your program so that if you add a new library outside of Lua or LÖVE, Lua knows where to find it. To do that, you must know where to find the libraries yourself.
You told Luarocks to install inifile to local, so you know where to start. There are two easy ways to find the actual code of the library you installed: the ls command and the find command.
At the very bottom of the list is the inifile.lua file, which is—as its file extension .lua suggests—the Lua library that you seek.
Note
There is a related package path called package.cpath that locates complex libraries written in the C programming language. These libraries use the file extension .so on Linux, .dll on Windows, and .dylib on Macs.
This simple statement sets package.path to be whatever it already is, and then appends (..) the local directory. It also replaces any instance of ? with whatever is required.
Note
If you read other people’s Lua code, you might see the alternate method of pointing Lua to a library. Sometimes, a programmer provides the path to the library manually in the require statement, using dots as delimiters: require('lib.inifile.inifile'). This isn’t wrong or bad, but it is very specific to a single library file. Not all libraries consist of just one file, so that method is less flexible than providing the package.path.
Lua has parsed, read, and written a plain text configuration using a local library.
Deck Building
Having completed the exercise in this chapter, you not only know how to store data on your user’s computer, but you also know how to define data structures in a file to have it imported by your application. That means you don’t have to define a deck of complex battle cards in the main code of your application, which means a smaller file for your executable code and a lot less clutter in your main script.
For the Blackjack game, the card deck was a simple 52-card poker deck. Your current project, Battlejack, can use a standard card deck, but part of the fun of programming digitally is that you can generate game assets without the costly manufacturing bills involved in creating a new deck of cards in the physical world. It doesn’t make sense to limit the game to a standard poker deck when you can invent any theme you want for your game.
Regardless of your artistic skill or access to artwork, 52 cards is a lot of cards to make. It’s not impossible (there are more than 10,000 Magic: The Gathering cards, and 2,000 in Magic Online), but for an independent game developer, it’s a tall order. When determining the assets for a game, it’s important to look critically at what is necessary and what is just nice to have.
For this project, even though the design assets were 52 cards, there were actually only 10 unique values: 1 through 10. For every iteration of cards 1 to 10, there were three cards worth 10 (Jack, King, and Queen). Furthermore, although the dev deck had four suits, the suits actually had no effect on the game, so those can be thrown out.
To create player identities, the alpha version of the game used red and black, and since that’s easy and classic, the digital version can keep that. For accessibility, the digital version will also use a symbol along with the opposing colors, since not everyone can see the color red.
To make the game a little more exciting, the digital version of Battlejack will enable players to “level up” as they continue to play. Levels in any game are a mix of rewards and, essentially, penalties; the players level up and become more powerful, but only to face new challenges. That means the digital deck will have a small subset of extra cards that are shuffled into the game, throwing off the predictability of how often certain values are drawn, and a second subset of cards that serve as “power ups” granting the player a free bonus (anywhere from a +1 to +3) to their hand.
There isn’t much to this dataset, but it should be expressed separately from the user data because it is not data that is meant to change. It defines the virtual deck of cards, and that’s all. Tracking which cards have been drawn during a game must be done by the application itself, because it gets reset every time a new game is started. Any permanent rewards, penalties, or level data is written to the user data file.
Take a moment to think about what kind of deck you want to use for your Battlejack implementation, or download and use the deck provided with this book. (The art is licensed under a Creative Commons license, so you may use the artwork for any purpose). Once you have decided on a theme, create a project folder called battlejack. Create font and img directories within your project folder, as usual.
That’s just 25 cards to generate; although in a pinch it can be done with just three (one for all face cards, one for all earned cards, and one for all powerup cards). That’s manageable, so now all that’s left to do is make them. And that’s just what you’ll do in the next chapter.
Homework
Installing and learning new libraries is an important part of programming. Nobody codes everything from scratch unless absolutely necessary, because there are so many great libraries out there with much of your work already done for you.
INI files store key and value pairs; one word correlates to exactly one other word or number. For more complex data structures, there is a format called YAML, which allows you to define multiple levels of information for everything in your data set.
Install lua-yaml or a similar library for parsing YAML and try to parse some sample data.
For an example of a working script, download the code files for this chapter from this book’s code repository.