Motivation
What the Rule System
Taking a task to recreate a simple game built with HTML and JavaScript into a production-ready game led me to re-evaluate its core components. Among them, the rule system, urging me to find an approach for clarity, flexibility, and maintainability. I'll weave through snippets of code and narrative, unraveling the creation of a game's rule system.
Why the messy structure
The conventional if this then that
structure felt messy, prompting a search for an elegant and maintainable alternative.
One solution
How to maintainable alternative
By defining a set of message types using TypeScript enums, such as 'Ok,' 'NoFire,' and 'NoMove,' numeric values assigned to each type facilitated a streamlined evaluation process, laying the groundwork for a rule system, where 0 is OK, 0 is converted to false. That means that we can set our mental map to if not [OK, 0, false]
there is an issue.
Mapping Rules for Simplicity
export enum ResultType {
Ok = 0,
NoFire,
NoMove,
NoFuel,
FieldCantCreate,
NoResources,
NoBase,
BaseNotInRange
}
Knowing that the following code:
var result;
if(!false){
result = codeWillRun()
}
can be written with logical operation OR:
var result = false || codeWillRun()
as effectively the same, we can do a lot of designation making by adopting strategy to return 0 if all OK.
var result = rule1()||rule2()||...||ruleN()
if(result == 0){
// all rules satisfied
}
We can have as may rules as we want and it will all fall in its place with ease.
Example of create vehicle rules
Now a real thing, the rules for Vehicle Creation
export function canCreateVehicle(gameState: GameState, x: number, y: number, pl: number, vehType: number) {
return canMapCreate(gameState, x, y) ||
baseInRange(gameState, x, y, pl) ||
isTooCloseToEnemy(gameState, x, y, pl) ||
canAffordVehicle(gameState, pl, vehType);
}
The 'canCreateVehicle' function exemplifies the power of chaining rules using the logical OR operator (||). It elegantly expresses the conditions for vehicle creation, with each rule encapsulated in its respective function, contributing to a more modular and comprehensible code-base.
Each rule follows a similar approach, maintaining a consistent structure that extends down to the fundamental building blocks of the game โ primitive data types. This uniformity not only enhances the clarity and readability of the code but also facilitates a modular and scalable architecture. Whether examining the creation of vehicles, calculating distances, or ensuring result consistency, the underlying principles remain grounded in this structured and functional foundation.
Ensuring Result Consistency
export function isOk(res: ResultType) {
return res === ResultType.Ok;
}
To maintain result consistency, the 'isOk' helper function provides reminder that result should be OK. By centralizing the logic for checking if a result is 'Ok,' this function reduces redundancy, preventing potential misinterpretations, and contributing to the overall clarity of the code-base.
After-math
Drawbacks of using 0 for success
However, the practice of using 0 to represent success (Ok) and any other value to signify an error does have its downsides. Initially, it can be counter-intuitive, challenging developers to overcome ingrained expectations where 0 typically denotes failure. This unconventional approach may appear cryptic, particularly when using logical OR operators to chain rules. The reliance on the truthiness of non-zero values to indicate errors might lead to a mental mapping challenge, requiring developers to re-calibrate their understanding. While this approach enhances code organization and readability, it demands a shift in conventional thinking, necessitating a mental map to navigate the intricacies of rule evaluation.
In defence of using zero for positive
The decision to adopt the approach of using 0 to signify success and any other value as an error comes from a desire for a more informative rule system. In a binary system where 0 represents failure, each rule would convey a simple pass or fail status. However, this simplicity comes at the cost of losing granularity in error reporting. By designating various non-zero values to different error types, this approach allows for a more detailed understanding of rule failures.
Conclusion of a functional approach
In adopting a functional approach to game rule systems, the code-base underwent a renaissance, balancing simplicity and sophistication. Through Type Script Enums, logical operators, and nicely crafted functions, the rule system evolved into a consistent, explicit, and joyous entity. This approach is especially suited for turn-based games, not only addresses the initial implementation's shortcomings but paves the way for a more robust and enjoyable game making experience.
Top comments (0)