Hansa Teutonica – Post Mortum

Hansa Teutonica is nearing completion and I wanted to capture some of the lessons that we learned during development. Overall, the game has turned out to be a much larger project that we originally anticipated. We are very happy with the design that we ended up with and we think that future games can use the same design. The game is currently playable except for placing an office to the side of an exiting city. There are also some user interface improvements to make and quite a bit of testing yet to do. But the majority of the code is done.

What went wrong:

  • Duplicating the model: We started this project by creating a C++ model of the game logic. Then we built a GUI using the Torque 2D game engine and its scripting language. There is some overhead in passing data and calling functions between the two. In our first design the GUI had knowledge about the state of the game. It seemed like we needed the game state for the GUI to behave correctly. This was a big mistake. As we built the GUI, we realized that we were re-writing a bunch of the model and game logic code in torque script and the two models would get out of sync with each other. And since one person was writing the C++ model, and another writing the script model, they behaved differently too. We ended up re-designing and making the GUI dumb. See the first bullet in “What worked”.
  • Enums: We used C++ style enums throughout the model, but we didn’t bother to make them available on the GUI side. This was a mistake. We ended up spending more time translating back and forth between strings and enums and debugging problems than it would have taken to do it right from the beginning. On our next project, we will export all C++ enums to the script code as global variables like: $PIECE_TYPE::MERCHANT
  • “Action” buttons on the GUI: Our GUI design has a set of buttons for each of the actions that the player can take during their turn. This seemed like a good idea, and may still work OK for brand new players. But experienced players quickly find the buttons to be a burden. They want to skip the step of clicking the action button and just take the action. For example, it is easier to just click on the piece or on the board instead of first clicking the “Place Piece” button. There are a lot of cases where we wouldn’t need the action buttons at all. You can almost always figure out what the user wants to do when they click on the game piece they need to manipulate. In the next game, we will only have “action” buttons for cases where it is required.
  • Accidental touches: On the multitouch surface, accidental touches occasionally happen. The most common is to accidentally click with your sleeve. But people sometimes just touch the wrong spot, or are just trying to point and accidentally click. In a game as long as Hansa Teutonica, an accidental touch has to be undo-able.
  • GUI <-> Model translator class. We made one C++ class that does all the interaction between the GUI and the Model. Isolating the interaction was a good design, but the function signatures in this class used GUI friendly variable types. In the future, we will perform the translation from model types to GUI types in this same class and make the functions called by the model take model friendly types.

What Worked

  • Dumb GUI: See “duplicating the model” above for a description of the problem this solved. Instead of the GUI keeping track of the state of the game and updating the model, the GUI simply passes all the user actions back to the model and the model tells the GUI what to display and what dialog to show. When the model needs input from the players, it tells the GUI to request that input and provides a function to call when the player makes their selection. In the end, this ended up being extremely helpful. We were able to re-use a lot of GUI code for different parts of the game. More on that in the next section.
  • Callback system: Once we decided that the GUI would be dumb, we needed to tell the GUI how to call back into the model with the player’s actions. For example: If the player is clicking on a house on the board, it may be because they are placing a new piece, moving a piece, displacing an opponent, claiming a road or placing a bonus tile. The model know what state the game is in and what to do with the clicked house, but the GUI just knows that the user needs to click on a house and needs to pass that back to the model. To do this, the model passes a function pointer to the GUI for the GUI to call when the user makes a selection. The new C++ lambda functions made this very clean. Here is an example from the model where the user is placing a piece and we need to ask them which house to place it on:
    GameGUI::highlightLocations(validMoves);
    GameGUI::openSelectHouseDialog( [this](House *dest) { placePiece_House(dest); } );

    The first line tells the GUI to highlight the valid houses. The second tell the GUI to ask the player to click on a house and then to call a function with the clicked house. Since placePiece_House is a member function, we use an anonymous function that has access to the <this> pointer. Compared to member function pointers, this is very clean. Here is another, more complex example:

    GameGUI::openSelectTypeDialog(data->displacedPlayer->name(), true,
            [this, validHouses](Player::PieceType type) {
              GameModel* model = this;
              // Ask the GUI where they want to put the piece
              GameGUI::highlightLocations(validHouses);
              GameGUI::openSelectHouseDialog([model, type](House* dest) {
                model->addBonusPiece(true, type, nullptr, dest);
              });
          });

    Here I needed access to some of the local variables within my callback. This would have required new member data or a data passing structure without lambdas.

  • Move system: We knew that in a long and complex game like Hansa Teutonica, we would need to be able to save/load and undo moves. In our shorter games, we often don’t forgive mistakes. The game goes on and there is no way to undo. But in an hour plus game, you don’t want to lose because of a mistaken click. To implement this in the model, we used a standard pattern of keeping a stack of Moves. The Move class has the functions ‘do’ and ‘undo’ along with the ability to save and load itself from a file. Each move type is derived off the base move and adds the functionality that it needs to execute itself. This system ended up working well. Even though there was a lot of save/load code to write for all the moves, loading a game is just re-executing all the moves. And, once loaded, the undo stack is still available. There were a couple cases where the moves were multipart and we wanted the GUI to update after each part. So the do/undo paradigm had to be broken for those moves. As the model executes and the move is built, the GUI is updated. When the move is completed, doMove is never called. This is a bit dangerous because the doMove function is still used when the game is loaded.
  • No const: In my ‘professional’ career and on previous personal projects I always used const member functions and parameters. I ignored const on this project and haven’t regretted it. For a project of this size, using const adds effort for almost no benefit. It is nice to know that a function wont change an input, but the cost of maintaining it is too high.
  • Board generation in the GUI: The GUI needed to know more about the board than the model since it needed the on screen location of all the elements. So we put board building in the GUI. As the GUI builds the board, it makes calls into the Model to add cities/roads/etc. And, by keeping the board building in the GUI, it is easier to move a city or road.

3 thoughts on “Hansa Teutonica – Post Mortum”

Leave a Reply