All Maps Are Not Created Equally

For this, the final blog post of the Capstone project, I’ve decided to utilize this space and time to gain a deeper understanding of maps and, more specifically, the boost container called flat_map. 

Recently in the Mahjong codebase I’ve been working with nested maps and accessing data stored in a nested map structure. Although maps were obviously a topic covered throughout my CS coursework, I honestly didn’t reach for them too often in my own projects and it took a fair amount of brainpower to unpack both what was in the nested map structure I was looking at and how to get the data from it that I needed.

Fast-forward through conversations with engineers, cppreference reading, and a supremely helpful video about maps in C++ from the Cherno and I succeeded in strengthening my understanding of maps, how to use them, and when to reach for them. But then, as often happens, my mentor/sensei asked me, “why do you think we’re using a boost container flat_map instead of just std::map?” to which, in the moment, I could only make assumptions and guesses around optimization, but nothing concrete. Back to the books for me!

First off, what the heck is a boost container flat_map? A flat_map is “an associative container that supports unique keys and provides fast retrieval of values of another type T based on the keys” (source). That sure sounds like a map… 🧐 Like std::map, flat_map also supports random access iterators.

Where I started to uncover some differences is in the following:

  • flat_map <Key, T> has a value_type of std::pair<Key, T> whereas std::map<Key,T> has a value_type of std::pair<const Key, T>
  • flat_map is implemented like an ordered vector, which means that insertion of new elements will invalidate previous iterators and references
  • Random insertion into a std::map is faster than random insertion into flat_map (source)
  • Random search between the two containers is comparable, though there is some debate that flat_map can be made faster and std::map cannot (source)

None of this necessarily answered my sensei’s question of why use a flat_map vs. a map. Thankfully though, a thorough and multiply cited above Stack Overflow post uncovered the answer to my query!

One very important, significant advantage of a boost container flat_map over std::map is the speed with which a flat_map can be iterated over. Although a std::map would work for the specific use case in our Mahjong game where we use a flat_map, the fact that our nested map structure needs to be iterated over in a couple of different places in the game to get the data we need, a flat_map is a much better choice because that iteration can be optimized.

Of course this is part of the job of a software engineer, however it’s still amazing to me that someone knew to or at least took the time to look into making that design decision. I am such a novice and constantly humbled by the intelligence and experience of my colleagues. It’s an incredible honor to work and learn from them, and I am eternally eager to continue growing in this field.

Buttons, You Really Push My Buttons

I kid you not, I have more to say about buttons.

Previously I experienced success with creating a grid of buttons that would display on the screen, but they didn’t do anything yet. Ultimately, I wanted to have users be able to press a ghost tile button and for the tile to register being touched by turning on like a light switch going from quarter opacity to full opacity. 

I began by utilizing a member function that allows the opacity of buttons to be set, which seemed like a surefire way to get the opacity of the buttons to change on press, but when running the Board Editor on a device I saw no inkling of an opacity change. When running the debugger I was able to confirm that the opacity function was indeed being entered and called on the button pressed. Yet again, a mystery was afoot!

Turns out, instead of setting the opacity of the ghost tiles when they were created, I was setting the lower opacity in the function that renders the view, which on the device at first looks just fine, but is very problematic since the rendering function gets called any time there is a change to the board, or any time it is “made dirty”. So, when a tile button was pressed, the opacity changed to full opacity, a change to the board occurred, and the render function got called, changing the button back to ghost state. The remedy was simple, once figured out, and voila, buttons were able to be pressed and turned on.

A few other things would also need to happen when a ghost tile would be touched, such as the counter incrementing and that tile actually being added to the board. Although I really should’ve focused on adding the tile to the board and working on the model side of things, I kept tinkering with the view and added the increment counter (and decrement counter) functions to the handler for the buttons so that the counter would increase/decrease accordingly.

This led me to discover a bug, which truly was more of a result of poor planning on my part. The accuracy of the counter in its current state requires that the designer use the board editor in a very specific way. They start in add mode by default and can immediately tap on ghost tile buttons to get them to appear at full opacity and the counter to increment. To remove a tile and have the counter remain accurate, a designer must press the remove button and then click the tile they want removed. However, currently, a tile that is at full opacity will go back to quarter opacity when tapped, regardless of the current mode. 

Various discussions with engineers illuminated for me how and why the counter should be tied to the current/latest board in the boards vector instead of being tied to the number of touches on the screen. This makes a whole lot of sense when you think about it–who knows how many tiles there are at any given time? The current board! In practice though, it took me a while to absorb this and rather than letting the counter be something implemented later, I impatiently wanted it “to work” now. My way of doing it works in theory, but not well in practice.

For now, the buggy counter will live on and I’ll ignore it while I get to work on what should’ve come first, the model.

How to Add a Vector to Another Vector, or Things I Know But Struggle to Apply

This week I dabbled with creating the counter label and some functionality for the previously made add/remove buttons, but once again I spent the bulk of my time working with buttons. 

With much of the base UI complete, I needed to really start getting into the nitty gritty of getting tiles placed where the designer pressed on the screen, but I had one big question–how on earth do I do that? Posing my query to another engineer over coffee, they posited that one way I could accomplish this would be by turning the screen into a grid of buttons. My wheels immediately began spinning and I was keen on bringing this idea to fruition. 

Coding alongside my mentor, we started by creating a single button that used our tile texture so that the button on the screen looked like one of our blank Mahjong tiles. Having created together a single tile button that appeared on the screen at the specified point {{x,y}, z}, my mentor challenged me to extrapolate that into a function that would return a unique pointer of a TileButton and to then use that function to create a grid.

As per usual, bumps were encountered along the way, but eventually I had written a function named CreateButton that took in the rect of the board and a TileId, and returned a TileButton that could be pushed onto the Views vector. On the screen it looks a lil something like this:

Next I needed to create a grid of tile buttons, which would be a vector of unique pointers of TileButton. Filling the vector was pretty easy as I was able to push a new TileButton onto the button vector using the CreateButton() function I’d written, like this:

buttonVector.push_back(CreateButton(rect, tileId));

Getting the buttonVector into the views vector to display on the screen proved to be an embarrassing challenge for me. As I’d previously done with adding other elements to the views vector, I added a line in my GetActiveSubviews function:

views.push_back(&*buttonVector);  

The compiler was not pleased and gave me an error I didn’t understand to which I turned to Google and Stack Overflow for assistance. This got me nowhere. 

After a lot of Googling and searching through the codebase for a similar situation, I reached out to my project mentor via Slack to talk through the issue. I described that I was trying to add a vector<unique_ptr<T>> (where T is a subclass of View) to a vector<View*>. I was consistently getting hung up on how to “add” one vector to another, as though it had to be something particular with one vector being filled with unique pointers causing .push_back(vector) to not work and that I needed to use type casting or perhaps a different function for adding the vector. 

But as you surely know, vector doesn’t have a function that allows you to insert another vector into it. To add a vector to another vector, you need to implement a for loop that moves each item in the vector to the other vector (which, if vector did have a function to add a vector to itself, that’s how it would have to be implemented).

This “Aha!” moment left me with a lot of mixed feelings. I was pleased to be moving forward with pushing my vector of buttons onto the views vector by adding each button to the views vector, relieved that my code compiled, and satisfied that the grid of buttons had been created. But I was also feeling a bit of shame that I didn’t clue into this elementary concept sooner and without so much hand holding. I know that a vector is essentially just a list and that if you want to put each item from one list into another you are essentially looping through to do the job, but for some reason this knowledge was just out of reach at the time of application. 

This likely won’t be the last time some coding knowledge that I’ve previously learned doesn’t come to the forefront of my mind when it needs to, and I’ll continue to work on being kind to myself when it occurs; in the end, the goal is still to learn and grow, and this week I certainly learned something important about vectors. 

The latest iteration of the Board Editor with a grid of “ghost” tiles for designers to select and add to their board design:

Where to begin?

This project has been a long time coming, not only within the actual CS program at OSU as the bookend to an educational milestone, but also at work. I proposed the idea of completing my capstone project through Brainium back in the summer of 2021 and the wheels moved swiftly, from bringing this up to my engineering manager as a neat and mutually beneficial way for me to shift into the software engineering role to having the specs of an oft-sidelined project fully fleshed out and a mentor designated. Everything was set on the company side by early last fall and from there I simply had to wait, until now…

Being perhaps overly eager and not the most patient individual, I immediately jumped into Asana and spit out tickets, translating the specs of a MVP into task cards and identifying a laundry list of code components I could do away with once I created a clone of the project repository to make my board editor from. This was my first mistake.

Prior to “Potential Code Components to Disable” I had a card for “Code to Remove”

As I continued combing through the existing code for Mahjong, I continued to find large swaths of code that would simply not be needed in the Mahjong Board Editor I was to build. 

  • In-App Purchase functionality? Let’s get rid of it! 
  • Localization strings? We won’t need those for making Mahjong boards! 
  • Daily Puzzle logic? Once again, cut it out! 

My list grew with all of the pieces that I could eliminate from my cloned project, seemingly making improvements by reducing code and only holding onto what was really necessary for my project. I had it all planned out on my Asana board and included in my initial project setup the subtasks “Create new repository” and “Eliminate unnecessary code (see card)”.

Excited by this early, albeit minimal progress, in my weekly one-on-one with my engineering manager I shared my Asana board and asked to discuss making a new repository on Bitbucket. This is when I realized I definitely hadn’t thought everything through.

Although creating a new repository may have been nice, my manager pointed out that in the long run this would be problematic as changes made to Mahjong would not be applied to the Mahjong Board Editor. Hindsight is 20/20 and this should have been obvious to me, but in my enthusiasm to hit the ground running I hadn’t considered this key detail. Our conversation illuminated a better route—keep all the guts of Mahjong and instead create a new target. Much in the way our apps have a paid and unpaid target, we could create a new target for the Mahjong Board Editor that is in the same project. 

This seemed like the right way to go and I began talking with other engineers about making a new target, the consensus was that it would be tricky, but doable. Logan, a Principal Engineer at Brainium and my capstone mentor, agreed that this would be one route, but he had another idea for how to proceed with the setup and legacy of the Mahjong Board Editor.

Rather than creating a new target, in Xcode we could create a new custom build scheme. Our projects already include a few different schemes like DEBUG and QA, which incorporates a QA menu and various functionalities like advancing to the end of a game, and adding another scheme would not only be easier to do, but also allow for all of the benefits we want with having the Board Editor project exist in the same repo!

And so we’re off to the races with tomorrow marking the start of the Mahjong Board Editor Project and getting to shift some of those task cards from “To Do” to “In Progress”. Wish me luck.