Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - bddap/automata: Cellular automaton simulation in your terminal. · GitHub
Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - bddap/automata: Cellular automaton simulation in your terminal. · GitHub
Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - bddap/automata: Cellular automaton simulation in your terminal. · GitHub
Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - bddap/automata: Cellular automaton simulation in your terminal. · GitHub
Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - bddap/automata: Cellular automaton simulation in your terminal. · GitHub
Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - bddap/automata: Cellular automaton simulation in your terminal. · GitHub
Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - bddap/automata: Cellular automaton simulation in your terminal. · GitHub
Skip to content

Latest commit

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Preface

After reading the excellent book, "Game Programming Patterns" by Robert Nystrom, I created a toy program to experiment with concepts from the book.

This report:

  1. Documents the inner workings of the toy program.
  2. Explains some patterns from Robert Nystrom's book and details their use in the program.

Overview

Automata, the program, is inspired by Conway's Game of Life. Both feature a 2d grid, on which Finite States are displayed as square cells. Each frame, cells on the grid are updated according a Finite State Machine. This application of FSM's is often referred to as Cellular Automaton. Automata, the program, gets it's name from Cellular Automata.

Automata differs from Conway's Game of Life. While cells in Conway's Game of Life may occupy one of two states. Automata's cells may occupy one of 521 states. In the section on State, we'll explore application of the State pattern in the program.

Automata program in action:

https://asciinema.org/a/410093

Legend:

ColorAtomata State Name
BlueWater
Light GrayAir
WhiteRedstoneBlock
Light RedHigh Powered Redstone
Dark RedLow Powered Redstone
Dark GrayUnpowered Redstone
GoldSlug
Dark YellowSlime

Program Structure

The Automata program is written in the rust programming language.

Source code for the automata program may be found here: https://github.com/bddap/automata

The program is separated into four files/modules. Main, Automata, Automata Field, and Graphics.

Main

Initializes an Automata Field and graphics. Runs a Game Loop to Update Automata States and refresh graphics at a regular interval.

Automata

Defines the core Finite State Machine. Game logic is implemented here.

The term "Automata" is overloaded in this report. "Automata" may mean one of two things.

  1. The toy program which this report details.
  2. The data structure used to represent a cell on the Automata Field.

Please use context to determine which meaning is intended.

The Automata data structure is an enum defined thusly:

pubenumAutomata{Redstone(u8),Water(u8),RedstoneBlock(),GameOfLife(bool),Air(),Slug(Direction),Slime(),}

Rust enums allow for associated data. Redstone, Water, GameOfLife, and Slug each include extra state: power, depth, active, and direction of movement respectively.

Redstone is inspired by, and behaves somewhat similarly to, Minecraft's voxel of the same name.

Water flows over neighboring blocks.

RedstoneBlock provides redstone power.

GameOfLife transforms into a redstone block when powered.

Air does nothing.

Slug travels across the grid.

Slime is left in a trail behind Slugs.

Automata Field

Automata Field represents a two dimensional grid of Automata. A double buffer is used to prevent race conditions between cells.

Graphics

Graphics prints colored squares to the terminal as part of the Game Loop. Terminal graphics are employed to simplify development (no windowing libraries necessary).

Patterns

Five patterns from "Game Programming Patterns" were employed when writing the Automata program.

State

Game Programming Pattern's chapter on state teaches us how to use finite state automata to manage the behavior of in-game objects such as player characters or NPCs.

Modeling behaviors as FSMs makes game code less verbose, and makes a much easier to reason about.

The book gives an example of how state machines can save a game from bugs--translated into rust.

enumInput{PressB,PressDown,ReleaseDown,}
...
fn handleInput(&mutself, input:Input){match input {PressB => self.jump(),PressDown => if !self.isJumping{self.setGraphics(Ducking)},ReleaseDown => self.setGraphics(Standing),}}

The bug in the above program occurs when someone presses B in mid-air. The above, non-FSM code will allow jumps even when the player is in the air.

Here is an example of the state pattern in action:

enumPlayerState{Standing,Jumping,Ducking,Diving}
...pubfnhandleInput(&mutself,input:Input){self.state = match(self.state, input){(Standing,PressB) => (self.velocity.y = 1.0;Jumping),(Standing,PressDown) => Ducking,(Ducking,ReleaseDown) => Standing,(s, _) => s,}}

While the bug is avoidable without a state machine, it's much easier to catch when using the the State pattern.

Automata uses the State pattern to model cell behavior. In fact, cell behavior is completely defined as a single state machine. Automata's state machine is described as a function called next_middle. next_middle takes the surrounding cell states as input, and returns the next state of the middle cell.

pubfnnext_middle(surroundings:Surroundings) -> Automata{ifletSome(next) = surroundings.infliction_requested(){return next;}match surroundings.middle{Water(0) => Air(),// No water => AirWater(wetness) => Water(wetness.max(1) - 1),// Water drains over timeRedstone(pow) => Redstone(pow.max(1) - 1),// Unpowered redstone goes darkSlug(_) => Slime(),// Slugs leave a trail of slime
a => a,// Everything else stays the same}}

Cells may only modify themselves. This limitation reduces race conditions, but imposes a limitation on Automata. Namely, how does one Automata impose a change on it's neighbor? Consider the state:

Slug(Direction)

We want this slug to crawl over every Automata in it's path. In other words, every state update, the slug needs to turn the automata it faces into a slug, and turn itself into slime. This is where the infliction_requested() method comes in. infliction_requested() asks each surrounding automata, "Do you want to change me?", if any answer yes, the middle automata accepts the state given.

infliction_requested() calls inflict() on each surrounding Automata to find out whether a change of state is requested. Here how the slug destroys all in it's path:

fninflict(&self,other:Self,direction:Direction) -> Option<Self>{matchself{
...Slug(slug_direction) => if slug_direction == direction {Some(Slug(slug_direction))}else{None},
...}}

When neighboring Automata request an infliction, one of the inflictions is chosen using a deterministic set of rules. The rules are essentially a ranking system, the requested state with the highest rank is selected. Here's what happens when two slugs collide.

fn resolve_infliction(&self, other: Self) -> Self {
match (*self, other) {
(Slug(_), Slug(_)) => Slime(),
...
}
}

They splat, turning into slime.

Double Buffer

Double buffers commonly serve one of two purposes:

  1. Prevent presentation of state while it is being mutated.
  2. Prevent race conditions while mutating state.

The Automata program uses A double buffer for the latter.

A double buffer holds two copies of some data, primary and secondary. One copy is mutated, while the other copy is used for something else.

In our case, the double buffer represents a grid of automata. Automata do not mutate their own state. Instead, they return a new Automata which is then written to a secondary buffer. While game state is updating, the primary buffer is input, and the secondary is output. Each game tick, the primary and secondary buffers are swapped.

Game Loop

The Automata program employs a naive game loop.

  1. State is updated.
  2. Game is rendered to the user.
  3. Process is repeated.

Data Locality

Data locality is a performance optimization. Avoid unpredictable memory access and your processor will thank you with a speed boost. Keeping your game state in a contiguous region of memory can increase performance significantly.

The automata program definitely does not need any performance optimization; it runs far faster than needed. That said, the program does benefit from data locality. Game state is stored directly in a pair of standard vectors. Only two heap allocated structures are are used to store the automata grid.

Dynamic dispatch is also avoided. Enums an switch statements are used in place of virtual classes.

Update Method

Automata Field has an update method which is called each frame. It's called tick(), but it does the same thing.

pubfntick(&mutself){for x in0..self.width{for y in0..self.height{self.field_alternate[y asusize*self.widthasusize + x asusize] = next_middle(self.surroundings_for(x, y))}}
mem::swap(&mutself.field,&mutself.field_alternate);}

tick() computes the next game state, writing the results to a secondary buffer, then swaps secondary and primary buffers according to the double buffer pattern.

What I Learned

Rust is a really nice language to work with. The rust compiler is a mentor, strict but kind, always trying nudging you in the right direction.

While cleverness should often be avoided in programming, sometimes the reduction of complexity code provides make cleverness worthwhile.

Research best practices and defacto standards before inventing your own solutions. Lots of other people probably grappled with similar problems in the past, and you will likely find a more elegant, time tested solution.

I learned how to make video games! And maintainable ones at that.

Works Cited

Nystrom, Robert. Game Programming Patterns. Self Published, 2014. gameprogrammingpatterns.com

Try it yourself!

What to run the game on your own machine? Here's how:

git clone https://github.com/bddap/automata.git
cd automata
cargo run

About

Cellular automaton simulation in your terminal.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages