//! ====== //! file: mario_state_machine.zig //! This is an example of a Mario/Powerup state machine. //! It showcases the usefulness of switches and tagged unions in Zig. //! See state machine diagram: //! https://external-preview.redd.it/TgwKB-bdEWJase06sIDXmVtaGaP7AZTD9YKn0x4yUWo.png?auto=webp&s=c0318b178038bd83212392c8fdd16e1a4b1a0049 //! ====== /// This is a tagged union. /// See tagged union doc: /// https://ziglang.org/documentation/master/#Tagged-union /// Each union "tag" becomes part of an enumeration, and it means we can use the union in a switch expression. /// Combined with Zig's exhaustive switches, this becomes a very useful pattern! /// /// A union can only have a single "tag" active at any time. Attempting to read from an inactive tag throws a compile /// error, making them a good tool for implementing state machines. pub const MarioMode = union(enum) { dead: void, // void tag type makes it just like a basic enumeration. mario: void, super: SuperMario, cape: CapeMario, fire: FireMario, /// We could have had MarioMode as a basic enum, but the tagged union allows us to attach different data payloads /// for each "tag". Note unlike C unions, the in-memory representation isn't guaranteed. For that you use /// "extern union" or "packed union". pub const FireMario = struct { fireballs: u8, /// Putting functions inside a struct/enum/union is an organization tool, effectively namespacing the function. /// We also get some syntatic sugar with the first parameter, effectively making it a method. pub fn fire(self: *FireMario) u8 { if (self.fireballs == 0) unreachable; // The program will panic if execution reaches "unreachable". // Without this we would still get an integer overflow panic // if it attempted to subtract 1 from 0. This just states your // intentions more explicitly. self.fireballs -= 1; return self.fireballs; } }; /// Our payload structs only have a single field each, so the tag types could have used f32/i32 directly, but you /// could imagine how over development iterations the payloads could end up with more complicated data. pub const CapeMario = struct { duration_left: f32 }; pub const SuperMario = struct { bonus_health: i32 }; // You can also declare constants within an union/struct/enum, // another useful namespacing tool. pub const super_default = MarioMode{ .super = .{ .bonus_health = 16 } }; pub const cape_default = MarioMode{ .cape = .{ .duration_left = 4.5 } }; pub const fire_default = MarioMode{ .fire = .{ .fireballs = 10 } }; /// This is the state machine. Select the next mode, based on the current mode and "transition" (the powerup). /// The switch statements here are exhaustive, so if you add new tags to either MarioMode or MarioPowerup you will /// get a compile error if you do not also add switch cases. /// See switch doc: /// https://ziglang.org/documentation/master/#switch /// /// This makes switch expressions much more useful than in other languages, where the danger of using the /// enum+switch paradigm could have programmers easily add new enumeration tags and forget to update switches /// scattered across the codebase. /// /// Note the return type is ?MarioMode, this states that this function will either return MarioMode or null. /// See Optionals doc: /// https://ziglang.org/documentation/master/#Optionals pub fn nextMode(mode: MarioMode, powerup: MarioPowerup) ?MarioMode { // Switches can be used as expressions, making them even more useful. return switch(mode) { .dead => null, .mario => switch(powerup) { .mushroom => super_default, .feather => cape_default, .flower => fire_default }, .super => switch(powerup) { .mushroom => null, .flower => fire_default, .feather => cape_default }, .cape => switch(powerup) { .mushroom, .feather => null, .flower => fire_default }, .fire => switch(powerup) { .mushroom, .flower => null, .feather => cape_default } }; } }; /// A basic enum. /// This will be backed by an unsigned 8-bit integer. You could omit the (u8) to have the compiler infer the backing /// type. /// See enum doc: /// https://ziglang.org/documentation/master/#enum pub const MarioPowerup = enum(u8) { mushroom, feather, flower, }; pub const MarioCharacter = struct { mode: MarioMode = .mario, health: i32, /// Note the "*const" parameter here, this is a pointer to immutable data. Pointers in zig are guarenteed to not be /// null, so we don't need to check for nullptr here. Nullable pointers are expressed as "?*". pub fn getHealth(self: *const MarioCharacter) i32 { return switch(self.mode) { // Switching on a tagged union lets you then capture the tag's payload. The capture is expressed in // "|mode_state|" and gives us const data. For mutable data you would express it as "|*mode_state|". .super => |mode_state| self.health + mode_state.bonus_health, // The "else" case is called for all other tags not counted for, this satisfies the constraint that all // switches must be "exhaustive". else => self.health }; } pub fn givePowerup(character: *MarioCharacter, powerup: MarioPowerup) void { const next_mode_or_null: ?MarioMode = character.mode.nextMode(powerup); // This is the typical way to handle Optionals. If the Optional is not null, the actual data will be captured // into the following scope. if (next_mode_or_null) |next_mode| { character.mode = next_mode; } } pub fn useSpecialPower(character: *MarioCharacter) bool { switch(character.mode) { // This is capturing the union payload as a mutable pointer. "state.fire()" is syntatic sugar for // "MarioMode.FireMario.fire(state)". .fire => |*state| { if (state.fire() == 0) { character.mode = .mario; } return true; }, else => return false } } }; // the "std" (standard) zig library has a lot of useful and common data structures and functions. This is cherrypicking // the assert function out of it. const assert = @import("std").debug.assert; // Zig comes testing system for writing and running unit-tests. You can run all tests in this file on the command line // via: // zig test mario_state_machine.zig. // The std library also has useful functions under std.testing. // See testing doc: // https://ziglang.org/documentation/master/#Zig-Test test "mario state machine" { var mario = MarioCharacter{ .health = 100 }; assert(mario.mode == .mario); assert(mario.getHealth() == 100); // "mario.givePowerup(.mushroom)" is syntatic sugar for "MarioCharacter.givePowerup(&mario, .mushroom)". mario.givePowerup(.mushroom); assert(mario.mode == .super); assert(mario.getHealth() == 116); mario.givePowerup(.flower); // Return values are explicit, if you don't intend to use them you must explicitly discard them with "_". _ = mario.useSpecialPower(); const did_fireball = mario.useSpecialPower(); assert(mario.mode == .fire); assert(mario.getHealth() == 100); assert(did_fireball); assert(mario.mode.fire.fireballs == 8); for (0..7) |_| { _ = mario.useSpecialPower(); } assert(mario.mode == .fire); assert(mario.mode.fire.fireballs == 1); _ = mario.useSpecialPower(); assert(mario.mode == .mario); assert(mario.useSpecialPower() == false); mario.givePowerup(.feather); assert(mario.mode == .cape); mario.givePowerup(.feather); assert(mario.mode == .cape); mario.givePowerup(.mushroom); assert(mario.mode == .cape); mario.givePowerup(.flower); assert(mario.mode == .fire); }