Skip to content

Instantly share code, notes, and snippets.

@karlrwjohnson
Created October 19, 2018 14:44
Show Gist options
  • Save karlrwjohnson/b977df1ece0604a713d78768ec206dbe to your computer and use it in GitHub Desktop.
Save karlrwjohnson/b977df1ece0604a713d78768ec206dbe to your computer and use it in GitHub Desktop.
Action pattern for turn-based games
// 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