Chapter 12

Sets, Maps, and Advanced Collections

Lists are the workhorse of game programming, but they are not the only collection worth knowing. Sometimes a list is the wrong shape for the job. If you want to track which keys a player has collected, a list lets them collect the same key twice — which makes no sense. If you want to look up a color by its name, a list forces you to search through every element to find it.

For these situations, Kotlin gives us two more collections. A set holds a group of values where every value is unique — no duplicates allowed. A map holds pairs of values, letting you look one up by the other, like a dictionary where you look up a definition by its word. Both solve problems that lists handle clumsily, and both will earn their place in our games.

We will also take a first peek at one of Kotlin's most beloved features: the ability to filter and transform a whole collection in a single line. We will only sketch it here, because it relies on lambdas, which we cover properly in Chapter 14 — but even the sketch is worth seeing.

In this chapter, we will:

Let's start with sets.

Sets

A set is a collection that automatically refuses duplicates. Add the same value twice and the second add simply does nothing — the set already has it. That single rule makes sets perfect for any "have we got this one?" question.

You create a read-only set with setOf, or a changeable one with mutableSetOf:

val collectedKeys = mutableSetOf<String>()

collectedKeys.add("gold")
collectedKeys.add("silver")
collectedKeys.add("gold")     // Ignored — "gold" is already in the set

println(collectedKeys.size)   // 2, not 3

In the preceding code, we add "gold" twice, but the set only ever contains it once, so its size is 2. With a list, we would have ended up with ["gold", "silver", "gold"] and three elements — almost certainly not what we wanted for a collection of keys.

The other thing sets are brilliant at is answering "is this value present?" using contains:

if (collectedKeys.contains("gold")) {
    openGoldDoor()
}

In a game, this reads beautifully and runs fast. Has the player unlocked this level? unlockedLevels.contains(5). Have they already destroyed this asteroid? destroyedAsteroids.contains(id). Sets are built to answer that kind of question quickly, even when they hold thousands of values.

One thing to know: a set does not keep its values in any particular order. It is a bag of unique things, not an ordered line of them. If order matters to you, a list is the better choice. If uniqueness matters and order does not, reach for a set.

Maps

A map is the most powerful of the everyday collections, and once it clicks you will use it constantly. A map stores pairs: a key and a value. You use the key to look up the value, exactly like a real dictionary, where you look up a word (the key) to find its definition (the value).

Think of a player's inventory. You want to ask "how many arrows do I have?" and get an answer instantly. That is a map from an item name (the key) to a count (the value):

val inventory = mutableMapOf<String, Int>()

inventory["arrows"] = 30
inventory["potions"] = 5
inventory["gold"] = 1200

In the preceding code, mutableMapOf<String, Int>() creates a changeable map whose keys are Strings and whose values are Ints — that is what the two types in the angle brackets mean: <key type, value type>. We then add three pairs using the square-bracket syntax. inventory["arrows"] = 30 reads as "set the value for the key 'arrows' to 30." If the key doesn't exist yet, this creates it; if it does, this updates it.

Looking a value up uses the same square brackets:

val arrowCount = inventory["arrows"]    // 30

Maps and null

There is a wrinkle here that connects back to our first look at null in Chapter 2, and it is important. What happens if you ask a map for a key it doesn't have?

val swordCount = inventory["sword"]     // There is no "sword" key...

The map cannot invent an answer, so it hands back null — Kotlin's value for "nothing here." That is why looking something up in a map gives you a nullable result. inventory["sword"] is not an Int; it is an Int?, which might be a number or might be nothing.

For now, we will sidestep this in two ways. When we are certain a key exists, we can use getValue, which returns the value directly (and crashes loudly if the key is genuinely missing, which is a fair trade when you know it should be there):

val arrowCount = inventory.getValue("arrows")    // 30, as a plain Int

And we can check whether a key exists before relying on it, using containsKey:

if (inventory.containsKey("sword")) {
    println("You are armed!")
}

That is enough to use maps comfortably today. The full, graceful way to handle the null that map lookups can produce — safe calls and the Elvis operator — is the first half of Chapter 14, and it is one of the things that makes Kotlin such a pleasant language to write.

Looping Over a Map

You can walk through every pair in a map with a for loop:

for ((item, count) in inventory) {
    println("$item: $count")
}

In the preceding code, each pass gives us both the key and the value at once, which we have named item and count. This prints each line of the inventory: "arrows: 30", "potions: 5", and so on. Maps shine anywhere you have named things with associated data — inventories, settings, score tables, or, as we will see in the next chapter, a palette that maps tile codes to colors.

A First Taste of Collection Operations

Before we finish, here is a glimpse of something that makes Kotlin programmers very happy. Suppose you have a list of enemy health values and you want just the ones still alive (health above zero). You could write a loop:

val healths = listOf(30, 0, 75, 0, 10)
val alive = mutableListOf<Int>()
for (h in healths) {
    if (h > 0) {
        alive.add(h)
    }
}
// alive is now [30, 75, 10]

That works, and there is nothing wrong with it. But Kotlin lets you express the same idea in a single line:

val alive = healths.filter { it > 0 }      // [30, 75, 10]

And if you wanted to, say, double every value, there is map (no relation to the map collection — an unfortunate clash of names):

val doubled = healths.map { it * 2 }       // [60, 0, 150, 0, 20]

That little { it > 0 } in the braces is called a lambda — a small, nameless piece of logic you hand to the collection to apply to each element. We are not going to explain it properly here; that is the job of Chapter 14. For now, just register that it exists and what it does: filter keeps the elements that match a condition, and map transforms every element into something new, each in a single readable line. When you see this kind of code in the wild, or from an AI assistant, you will know what you are looking at — and in Chapter 14 you will learn to write it yourself.

Choosing the Right Collection

We now have four collections in our toolkit. Here is the quick guide to picking one.

Use a List (or MutableList) when you have an ordered sequence of things and duplicates are fine — enemies, bullets, particles, the order of turns. This is your default, and the one you will use most.

Use a Set (or MutableSet) when each value must be unique and you mainly ask "is this one present?" — collected keys, unlocked levels, IDs you have already processed.

Use a Map (or MutableMap) when you want to look up a value by some key — an inventory of item-to-count, a settings table of name-to-value, a palette of code-to-color.

And use a plain array when the size is fixed and known and you want a touch more speed.

When you are unsure, ask yourself what question you will ask the collection most often. "Give me the next one in order" points to a list. "Do we already have this?" points to a set. "What is the value for this key?" points to a map. Let the question choose the collection.

Summary

You have doubled the size of your collection toolkit. A set holds unique values and answers "is this present?" quickly, with no duplicates and no particular order. A map holds key-value pairs and lets you look a value up by its key, which is perfect for inventories, settings, and palettes — just remember that looking up a missing key hands back null, and that getValue and containsKey get us by until Chapter 14 teaches the graceful way to handle it. You also caught a first glimpse of filter and map, Kotlin's single-line collection operations, and learned a simple rule for choosing the right collection: let the question you ask most often decide.

In the next chapter we put maps to work for real. We will build the Level Grid — a tile-based game world stored in a 2D array, with a map translating tile codes into colors — and you will be able to tap the screen to reshape the level live. It is the foundation that countless 2D games are built on.

AI Exercise (Optional)

Sets and maps are a great topic to explore with an AI, because choosing between them is a real design decision. Skip this if AI isn't for you.

Open your AI chatbot and paste in this prompt:

"I am a beginner learning Kotlin. I have just learned about sets (setOf, mutableSetOf, add, contains) and maps (mutableMapOf, looking up with [ ], getValue, containsKey, and that looking up a missing key returns null). I know lists too, but I have NOT learned lambdas or classes of my own yet. I'm making a simple game. For each of these three pieces of data, tell me whether a List, a Set, or a Map is the best fit and why: (1) the names of achievements the player has unlocked, (2) the high scores for each level, looked up by level number, and (3) the sequence of moves the player made this turn, in order. Then write a short Kotlin snippet for each one, creating it and adding a couple of entries."

The heart of this exercise is the reasoning. We are asking the AI not just for code, but for a justification of each choice — and those justifications are exactly what you should be forming in your own head. Achievements are unique (a set), level high scores are looked up by key (a map), and an ordered sequence of moves is a list.

When the answer comes back, check the AI's reasoning against your own before you read its code. Did it match? If it picked differently from you, work out who is right — sometimes there is more than one defensible answer, and arguing it through is where the real learning happens. If a choice seems wrong, push back: "wouldn't a Set lose the order of the moves?" Make the AI defend its decisions.