Created
October 19, 2018 14:44
-
-
Save karlrwjohnson/b977df1ece0604a713d78768ec206dbe to your computer and use it in GitHub Desktop.
Action pattern for turn-based games
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Let's define a couple of dependencies to get started. | |
interface GameContext { | |
// Methods which Actions can use to query the state of the game | |
} | |
interface ActionGameController { | |
// Methods which Actions can use to affect the game | |
// - In Redux, this is the dispatch() method and various actionCreators. | |
// - In Java, this is a controller. | |
} | |
// Okay, with that out of the way... The Action interface. | |
// (It turns out this is just the Command Pattern (https://en.wikipedia.org/wiki/Command_pattern) | |
// with extra methods abstracting-out parameters) | |
// | |
// As a game grows, the number of actions a character can take grows as well, so it makes | |
// sense to push it out into its own class. Classes make sense as an abstraction (as opposed to | |
// a single method call) because the UI needs to be able to query and validate parameters, | |
// and Action instances themselves might need to store a bit of context. | |
interface Action { | |
// Depending on the game, you might have multiple instances of the same action type | |
// (e.g. Melee Attack with my Bastard Sword vs. Melee Attack with my +1 Greataxe), and | |
// in a client-server scenario you need to be able to store and retrieve objects by ID | |
// rather than reference types provided by your language. | |
UUID getId(); | |
// Action objects are responsible for determining whether they can be executed. | |
// Before doing anything with the action, the UI needs to know whether to offer it to the user. | |
// The most obvious case for making an action unavailable is when there is no set of parameters | |
// that would make it valid; for example, you can't hit someone with your sword if there's no | |
// one in range, and you can't cast Fireball if you're out of mana. | |
// You might make an AbstractAction abstract base class that calls getActionParameters() and | |
// getActionParameterOptions() automatically for this method. | |
bool canTakeAction(GameContext gameContext); | |
// If action requires user-provided arguments in order to run (e.g. which coordinate do I | |
// cast Fireball into? or which enemy to hit?), this method returns the names of those parameters, | |
// and optionally, their types. | |
// This method does NOT take a GameContext because the parameters for a given action are fixed... | |
// at least I think so! I haven't tested this rule yet. | |
Map<String, Class> /* or Set<String> */ getActionParameters(); | |
// For a given action's parameter, this enumerates the options you're allowed to select, e.g. | |
// where you can build a city or which enemies to target. | |
// All parameters go through the same interface. You could make a bunch of getters | |
// for each individual parameter, but this generic getter exists because the client/server API | |
// can't know about every single action and parameter supported by the game; otherwise, the design | |
// won't scale. | |
// The third parameter is the set of parameters the user has selected so far. Options for one | |
// parameter may affect the available options for another; for instance, an attack may only be | |
// able to affect adjacent enemies) | |
<T> Set<T> getActionParameterOptions(GameContext gameContext, String name, Map<String, Object> selectedParameters = emptyMap()); | |
// Final point of validation before attempting the action. | |
// Might be unnecessary given getActionParameterOptions() will only give you valid parameter options. | |
bool canTakeActionWithParameters(GameContext gameContext, Map<String, Object> selectedParameters); | |
// Obvious. | |
void executeAction(GameContext gameContext, ActionGameController gameController, Map<String, Object> selectedParameters) | |
} | |
// Consider an Action object to be immutable after it is created. | |
// If it stores any data inside of it (e.g. which weapon is being wielded), that data is inherent to the action, | |
// not a selection made by the user. | |
// The fact that user selections are external to the action makes them reusable; you can use the same action | |
// multiple times per turn. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment