Skip to content

Master Controller

Riley Conlin edited this page Jul 21, 2019 · 1 revision

The master controller is an attempt to separate the game rules from the engine, while still allowing maximum control over the engine for (hopefully) any game type. The main goal being that no modifications should be made to main.py or other engine level files to develop future games but all game rules and be condensed into a single place. It is a basic object so it can keep track of logic controllers, variables, and have its own additional functions if need be. Here is a list of the existing methods and where they are called in the engine structure. All method names are subject to change because I don't like them that much.

boot()

give_clients_objects()

  • run once before the game starts
  • instantiates clients in the context of the game world

The purpose of this method is to provide each client all objects they will have control over during the game. In space sim, it would be the space ship. In disaster city, it is the city. It is also used for establishing more one-time only client information like the team name.

loop()

game_loop_logic()

  • generator function
  • controls game loop

This is a unique type of method called a generator function. It acts as an iterator and determines the next element when called, simply put. It is used as the main iterator in the engine and is considered the loop itself. For every item returned, pre-turn(), turn(), and post_turn() will run. The purpose being that not every generated game will abide by the same structure. The game_map.json has the state of the world, but not every world will change per turn. Sometimes, a static world is all that is necessary. This allows for the ability to have an iterator that can keep track of when the game is supposed to end and when it is supposed to move on if at all and provide the key to the engine so it can load in the correct state of the world. The current structure is fairly simple as the game map keeps track of disaster occurrences and rates which change per turn, so simply returning a single incrementing number will do while making sure it doesn't go over max turns.

Here's a few examples. Say it is a static map that is always under the same key, "map":{location of things}. Easy change. There will still need to be a tracker of ticks to prevent an infinite match but instead of returning the number, the key will be returned each time instead, just "map" a ton of times. The most complex type of iterator we would have would be something like dungeon delvers where there are a series of rooms that are maintained. What would likely happen is it would return a specific room but change based on information saved to the master controller and modified by incoming turn data. It would then say return "start" until it sees that a self.room_choice variable is set to RoomChoice.forward, where it would then reset the room choice and move forward a room, and then return "room_1".

pre_tick()

interpret_current_turn_data()

  • runs at the start of every turn
  • receives current turn information

This method receives the current world information and is responsible for decoding the contents. It essentially establishes the world and any changes that may have occurred. In the current context, it will see if any disasters have occurred, create the respective object, and give it to the player. It will also read in all of the disaster rates, modify them based on current sensor levels, and then give it to the player. It is important that in games that may have a static map in the game_map.json file that it doesn't waste time recreating objects that already exist.

tick()

clients_turn_arguments()

  • runs once per client every turn
  • responsible for sending clients their turn data

This method has the basic task of giving the client or clients their appropriate turn data. This means giving them a means of communication (the Action object), the objects they control (the City object in this case), and any more theoretical items like opponents, the world, or a bank account. This method will always received just a singular client because it also has the task of curating and obfuscating objects to make sure the client given does not have access to more information than it should. First and foremost, everything sent to the client should be a deep copy, which is simple thanks to the copy library. Next, each object given ought to be properly concealed or obfuscated on a per player basis. It may not mean a ton in a single-player context like this, but in the context of space game, each player would have only opponents within their scanner range revealed to them, and even then they would have specific variables hidden, such as engine ID. This both protects players from tampering by other players, and ensure control over their "vision".

turn_logic()

  • runs once every turn
  • enacts bulk of game rules

This method is probably the most concisely named though it encompasses the most work. This is essentially where all of what we would traditionally consider game rules are applied. It receives all clients and the world and is tasked with shoving them through all the other logic controllers. It will apply actions done by the players as well as actions done by AI, the world, or other worldly objects.

post_tick()

create_turn_log()

  • runs at the end of every turn
  • creates contents of game log

A simple method that creates a dictionary containing the state of the world and its actions and gives it to the engine to be turned into a game log file. It also needs to ensure everything is being converted to JSON properly.

game_over_check()

  • runs at the end of every turn
  • informs the engine whether to stop the game now or not

A simple boolean check that is used to tell the engine if it should stop at the current turn or move on. Useful if your game doesn't have a set number of turns (which most won't) and if the game needs to end before the max number of turns have been hit. The important part about this method is that the logic of whether a game over should occur or not actually isn't decided in the method. For this game, it's done in turn_logic(). It just utilizes a self.game_over variable so that anywhere in the master controller it can be determined if the game should end or not. This method simply returns the state when requested. This ensures that the current turn can always be completed so a turn log can be written or so other clients may take their turn in case it may change the ranking of the game.

*load() does not currently have any interactions with the master controller because it only loads in the entirety of the game map, and should only need the location from the config.py file.