Creating Rusty Checkers

This section has a very specific goal: build a new checkers module entirely in Rust WebAssembly that conforms as closely as possible to the interface of the hand-written one from the previous chapter.

Diving into Rust is no small feat, so to keep things from getting too overwhelming, you’re going to be building something you’ve already built. Hopefully, the powerful and expressive syntax of Rust will make the game easier to reason about, and you might even be able to add features and rules that weren’t in the last game.

This new version of checkers will expose the same kinds of functions to the host (JavaScript), but you’ll also be writing some code that determines the list of valid moves, something that was just too excruciatingly painful in raw wast code to write in the previous chapter. The Rust version will also have the same concept of turns so that it can be played from a console or virtually any other kind of client.

To get started, find an empty directory and type the following:

 $ ​​cargo​​ ​​new​​ ​​--lib​​ ​​rustycheckers
 Created library `rustycheckers` project

Edit the Cargo.toml file in the project’s root directory as you did for the last sample to tell the build system that this is a dynamic library. Once you’ve got that set up, you should be ready to start coding. If you’re using a version of Rust version 1.31.0 or newer, then your default Cargo.toml will come with a line in the package section: edition = "2018". This indicates the use of the “2018 edition”[13] which allows for some newer (and often simpler) syntax, as well as many new features.

Make sure you remove this line from your Cargo.toml. For now, I want to focus on the core parts of Rust required to interact with WebAssembly, and will defer the use of the 2018 edition until the end of the book after you’ve had time to get used to the “regular” Rust syntax. Unless otherwise stated, all Rust samples in this book are designed to run on 1.31.0 or newer, but use the “2015 edition” syntax.

Setting Up the Board

Open up the rustycheckers/src/lib.rs file and empty it. At the bottom of the file, add the following line:

 mod​ board;

Now add a file called rustycheckers/src/board.rs. Rust’s module system is hierarchical. Wherever you add mod [module_name]; in your code, think of that module as being injected at exactly that spot in the hierarchy. This allows you to nest modules, and it fosters some really good isolation and encapsulation practices, but it can be hard to get used to for people who come from languages with implicit or directory-first module hierarchies.

Each Rust library has a single root module, the name of which is specified in the Cargo.toml file (the name setting under the [package] section). By convention, the code for the root module is found in the lib.rs file, so by including mod board; in the top of lib.rs, we’re declaring a submodule named board that exists directly under the root. Elements in the board submodule can be referenced from anywhere using the rustycheckers::board prefix.

The Rust 2018 edition[14] (not used until the end of this book) has more flexible rules around referring to modules.

To start writing the code to manage the game board, the first thing you’ll want to do is write some code to manage a GamePiece. In the previous chapter, game pieces were just 32-bit integers that could be bitmasked for information. Now that you’ve got a little more power to play with, you can create an enum for piece color and a struct (Rust doesn’t officially have classes, though its structs can often behave like classes) to represent a single piece:

 #​[​derive​(Debug, Copy, Clone, PartialEq)]
 pub​ ​enum​ PieceColor {
  White,
  Black,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub​ ​struct​ GamePiece {
 pub​ color: PieceColor,
 pub​ crowned: bool,
 }
 
 impl​ GamePiece {
 pub​ ​fn​ ​new​(color: PieceColor) ​->​ GamePiece {
  GamePiece {
  color,
  crowned: ​false​,
  }
  }
 
 pub​ ​fn​ ​crowned​(p: GamePiece) ​->​ GamePiece {
  GamePiece {
  color: p.color,
  crowned: ​true​,
  }
  }
 }

Don’t worry about the derive macros for now. They just auto-generate some boilerplate code to deal with common things most data structures in Rust need, like the ability to be compared, copied, cloned, and printed out for debug purposes.

This game piece has two functions: new and crowned. The first creates a new game piece of a given color, while the second creates a new piece of a given color with a crown on top. The latter function will come in handy when you want to crown a piece on the board.

Next, you can create the concept of a Coordinate. In raw WebAssembly, you didn’t have anything to represent this other than just numbers. Creating a struct for a grid coordinate here should help simplify the code, reduce the number of parameters that get passed around, and generally make things more readable. Also, as you’ll see, it’s a great place to stick some preliminary logic to support game rules:

 #​[​derive​(Debug, Clone, PartialEq, Copy)]
 pub​ ​struct​ ​Coordinate​(​pub​ usize, ​pub​ usize);
 
 impl​ Coordinate {
 
 pub​ ​fn​ ​on_board​(​self​) ​->​ bool {
let​ ​Coordinate​(x, y) = ​self​;
  x <= 7 && y <= 7
  }
 
 
 pub​ ​fn​ ​jump_targets_from​(&​self​) ​->​ ​impl​ Iterator<Item = Coordinate> {
 let​ ​mut​ jumps = ​Vec​::​new​();
 let​ ​Coordinate​(x, y) = *​self​;
 if​ y >= 2 {
  jumps​.push​(​Coordinate​(x + 2, y ​-​ 2));
  }
  jumps​.push​(​Coordinate​(x + 2, y + 2));
 
 if​ x >= 2 && y >= 2 {
  jumps​.push​(​Coordinate​(x ​-​ 2, y ​-​ 2));
  }
 if​ x >= 2 {
  jumps​.push​(​Coordinate​(x ​-​ 2, y + 2));
  }
  jumps​.into_iter​()
  }
 
 pub​ ​fn​ ​move_targets_from​(&​self​) ​->​ ​impl​ Iterator<Item = Coordinate> {
 let​ ​mut​ moves = ​Vec​::​new​();
 let​ ​Coordinate​(x, y) = *​self​;
 if​ x >= 1 {
  moves​.push​(​Coordinate​(x ​-​ 1, y + 1));
  }
  moves​.push​(​Coordinate​(x + 1, y + 1));
 if​ y >= 1 {
  moves​.push​(​Coordinate​(x + 1, y ​-​ 1));
  }
 if​ x >= 1 && y >= 1 {
  moves​.push​(​Coordinate​(x ​-​ 1, y ​-​ 1));
  }
  moves​.into_iter​()
  }
 }

An example of destructuring to pull the x and y values out of a Coordinate structure.

Produce an iterator of Coordinates of all possible jumps from the given coordinate.

Produce an iterator of Coordinates of all possible moves from the given coordinate.

The jump_targets_from function returns a list of potential jump targets from the given coordinate. The move_targets_from function returns a list of potential move (adjacent space) targets given the current coordinate location. These functions will be used to calculate a list of valid moves for each turn. Note that in the raw WebAssembly version of checkers, we just did a quick, naive check to verify that a move was valid. We can up the complexity a little here.

The code pub struct Coordinate(pub usize, pub usize) defines what’s called a tuple struct. Rather than this struct having named fields, it instead represents a strongly-typed tuple with public fields. You can access those fields with tuple accessors .0 and .1, but most of the code in this chapter uses destructuring and pattern matching to get at the x and y values in a more human-friendly fashion. In this case, each of the two anonymous fields in this tuple struct are of type usize, which is the data type Rust uses for vector indexes, allowing code to be more safe and portable across 32-bit and 64-bit architectures. Rust’s standard library is replete with usages of this data type.

The list of potential target squares for a checker piece to move is pretty small. The preceding code creates iterators that expose these potential squares to callers. As you’ll see shortly, iterators can be chained with functions like map, zip, and filter to make for some fairly expressive syntax to define game rules.

Finish out the board.rs module by creating a struct that represents a game move. Here you can see one place where it was simpler to access the tuple values as fields rather than using a pattern match:

 #​[​derive​(Debug, Clone, PartialEq, Copy)]
 pub​ ​struct​ Move {
 pub​ from: Coordinate,
 pub​ to: Coordinate,
 }
 
 impl​ Move {
 pub​ ​fn​ ​new​(from: (usize, usize), to: (usize, usize)) ​->​ Move {
  Move {
  from: ​Coordinate​(from​.​0, from​.​1),
  to: ​Coordinate​(to​.​0, to​.​1),
  }
  }
 }

One more thing before getting into the details of writing the game rules: references. In the coordinate code, &self is a reference to the struct. In Rust, anything prefixed with & is a reference. Any assignment that isn’t a reference is a move. Unless you explicitly treat something as a reference, ownership will be transferred during an assignment, meaning the value in the “old” variable will no longer be there (and you’ll get a compile error if you try to access it). This “move by default” pattern takes a bit of getting used to when learning Rust, and if the difference between move and reference is a little murky, you might want to take a minute and do a little background research on Rust’s concept of ownership.[15]

Writing the Engine Rules

Now for the fun part... open up rustycheckers/src/lib.rs and add the following line of code to the bottom:

 mod​ game;

Create an empty file called rustycheckers/src/game.rs. This will give you a nice clean space to work on the game rules, and it illustrates a pretty common pattern in idiomatic Rust: modularization. I am partial to small, purposeful files that are easy to read and understand, and I feel like I spend more time thinking about modular structure in Rust than I have in other languages I’ve used (which is a good thing).

The first thing you’ll want is a struct to anchor all of the game engine functionality to the same place, and to give you a spot to maintain game state. If you hadn’t noticed, thus far you haven’t done anything remotely related to WebAssembly, and that’s deliberate. You’ll want to keep the WebAssembly border clear of debris and make sure that you are free to build a game on this side of the Wasm/Rust border with very little crossover or coupling.

Patterns like this where we actively avoid tightly coupling two separate concerns allow our code to be easier to read, easier to maintain, and easier to add more features and use for unintended consumers in the future without modification. This kind of tactic is often referred to as a facade or an Anti-Corruption Layer:

 use​ ​super​::​board​::{Coordinate, GamePiece, Move, PieceColor};
 
 pub​ ​struct​ GameEngine {
  board: [[Option<GamePiece>; 8]; 8],
  current_turn: PieceColor,
  move_count: u32,
 }
 
 pub​ ​struct​ MoveResult {
 pub​ mv: Move,
 pub​ crowned: bool,
 }

In the previous chapter, you managed game state by manipulating bytes with direct memory access. Here, not only do you have a traditional two-dimensional array, but the type of the piece is Option<GamePiece>. It will make your code eminently more readable when an empty square is represented by the match-friendly keyword None rather than just a 0.

Remembering that the functionality for a struct resides in an impl block, start writing the code to initialize the game engine and its state inside the impl GameEngine block:

 impl​ GameEngine {
 pub​ ​fn​ ​new​() ​->​ GameEngine {
 let​ ​mut​ engine = GameEngine {
  board: [[None; 8]; 8],
  current_turn: ​PieceColor​::Black,
  move_count: 0,
  };
  engine​.initialize_pieces​();
  engine
  }
 
 pub​ ​fn​ ​initialize_pieces​(&​mut​ ​self​) {
  [1, 3, 5, 7, 0, 2, 4, 6, 1, 3, 5, 7]
 .iter​()
 .zip​([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]​.iter​())
 .map​(|(a, b)| (*a ​as​ usize, *b ​as​ usize))
 .for_each​(|(x, y)| {
 self​.board[x][y] = ​Some​(​GamePiece​::​new​(​PieceColor​::White));
  });
 
  [0, 2, 4, 6, 1, 3, 5, 7, 0, 2, 4, 6]
 .iter​()
 .zip​([5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7]​.iter​())
 .map​(|(a, b)| (*a ​as​ usize, *b ​as​ usize))
 .for_each​(|(x, y)| {
 self​.board[x][y] = ​Some​(​GamePiece​::​new​(​PieceColor​::Black));
  });
  }

The &mut self parameter to initialize_pieces indicates that it can only be used by a mutable reference to a game engine. Further, it’s a sign to any developer that this function mutates state. In the engine’s constructor, we create a mutable instance of the GameEngine struct, and then call initialize_pieces to set the board up for play.

This function might seem confusing if you’re not used to Rust syntax. In the previous chapter, you initialized the board by manually placing every single piece with a different line of code. In the real world, we have tools like iterators and filters that can help us with these tasks.

The iter function converts an array of known x- or y-coordinate positions into an iterator, which can then be zipped (a function that merges two iterators into an iterator of tuples) with another iterator via the zip function. Next, the map function converts the coordinates from i32 to usize (the data type for array indexes in Rust—remember Rust doesn’t have implicit conversions). For each one of the newly zipped (x,y) coordinates, you can set the appropriate black or white piece using Rust’s anonymous function notation.

The next bit of code takes a top-down approach and contains the logic to move pieces on the board. If you enter this by hand right now, it won’t compile because you’re missing a bunch of helper functions. But I think it’s easier to understand what’s happening by looking at this logic first:

 pub​ ​fn​ ​move_piece​(&​mut​ ​self​, mv: &Move) ​->​ Result<MoveResult, ()> {
 let​ legal_moves = ​self​​.legal_moves​();
 
 if​ !legal_moves​.contains​(mv) {
 return​ ​Err​(());
  }
 
 let​ ​Coordinate​(fx, fy) = mv.from;
 let​ ​Coordinate​(tx, ty) = mv.to;
 let​ piece = ​self​.board[fx][fy]​.unwrap​();
 let​ midpiece_coordinate = ​self​​.midpiece_coordinate​(fx, fy, tx, ty);
 if​ ​let​ ​Some​(​Coordinate​(x, y)) = midpiece_coordinate {
 self​.board[x][y] = None; ​// remove the jumped piece
  }
 
 // Move piece from source to dest
 self​.board[tx][ty] = ​Some​(piece);
 self​.board[fx][fy] = None;
 
 let​ crowned = ​if​ ​self​​.should_crown​(piece, mv.to) {
 self​​.crown_piece​(mv.to);
 true
  } ​else​ {
 false
  };
 self​​.advance_turn​();
 
 Ok​(MoveResult {
  mv: mv​.clone​(),
  crowned: crowned,
  })
 }

The first thing move_piece does is compute the list of legal moves based on whose turn it is and the state of the game board. If the intended move isn’t in the list of legal moves (this is why you need the derive macros to generate equality tests for the structs), then the function quits early.

There’s a line of code in this function that uses the unwrap function. Ordinarily we shy away from this, but since we know that accessing that one piece will work because of validation, we can get away with it. In a production application and adhering to defensive coding practices, we would probably validate this instead of calling unwrap.

Next, the engine checks to see if there’s a game piece to jump on the way from the source to the destination coordinate. If there is, the jumped piece is removed from the board. The engine then performs the piece move by setting the old location to None. Finally, the function finishes by checking if it should crown the piece after it moved. Can you imagine how hard it would be to read or write the jump logic code in raw wast?

The move_piece function returns a Result. Similar to an Option, a Result can have two values: either Ok(...) or Err(...). Using a pattern match on a result is a clean way to handle and propagate errors back up the call stack. Pattern matching against optional values or result types is something that you might be familiar with if you’ve developed with functional programming languages or used functional programming styles.

Computing Legal Moves

Computing the list of legal moves is the most complicated thing this Rust code does. In the following code, legal_moves loops through every space on the board and then computes a list of valid moves from that position. This way, this function returns a list of every valid move that the current player can make (rusty checkers doesn’t support multi-jumps to try to keep the codebase legible and book-friendly).

 fn​ ​legal_moves​(&​self​) ​->​ Vec<Move> {
 let​ ​mut​ moves: Vec<Move> = ​Vec​::​new​();
 for​ col in 0..8 {
 for​ row in 0..8 {
 if​ ​let​ ​Some​(piece) = ​self​.board[col][row] {
 if​ piece.color == ​self​.current_turn {
 let​ loc = ​Coordinate​(col, row);
 let​ ​mut​ vmoves = ​self​​.valid_moves_from​(loc);
  moves​.append​(&​mut​ vmoves);
  }
  }
  }
  }
 
  moves
 }
 
 fn​ ​valid_moves_from​(&​self​, loc: Coordinate) ​->​ Vec<Move> {
 let​ ​Coordinate​(x, y) = loc;
 if​ ​let​ ​Some​(p) = ​self​.board[x][y] {
 let​ ​mut​ jumps = loc
 .jump_targets_from​()
 .filter​(|t| ​self​​.valid_jump​(&p, &loc, &t))
 .map​(|​ref​ t| Move {
  from: loc​.clone​(),
  to: t​.clone​(),
  }).collect::<Vec<Move>>();
 let​ ​mut​ moves = loc
 .move_targets_from​()
 .filter​(|t| ​self​​.valid_move​(&p, &loc, &t))
 .map​(|​ref​ t| Move {
  from: loc​.clone​(),
  to: t​.clone​(),
  }).collect::<Vec<Move>>();
  jumps​.append​(&​mut​ moves);
  jumps
  } ​else​ {
 Vec​::​new​()
  }
 }

There’s an interesting pattern used in the valid_moves_from function that uses the iterator functions created earlier and chains those results through filter, map, and collect:

 let​ ​mut​ moves = loc
 .move_targets_from​()
 .filter​(|t| ​self​​.valid_move​(&p, &loc, &t))
 .map​(|​ref​ t| Move {
  from: loc​.clone​(),
  to: t​.clone​(),
  }).collect::<Vec<Move>>();

This takes all of the potential move targets and filters them based on whether that target is a valid move for a given game piece, at a given location, for a given target. Then, for each of the valid moves, we convert the coordinate target (indicated by the ref t in the lambda, showing that we’re taking the lambda’s sole parameter by reference) into a Move struct. Finally, the resulting iterator is converted into a vector via collect and the “turbofish”[16] syntax collect::<Vec<Move>>().

The valid_moves_from function produces a list of Move instances that are valid from the given coordinate. In formal checkers, if a player has both a valid jump and a valid move, they must perform the jump. This method is structured to place valid jumps first so that you can add that kind of strict rule checking if you want. You could also add a new property to the Move struct to tag a move as a jump, which might help in adding multi-jump capabilities to the game.

You still haven’t added anything to the code that deals with WebAssembly. In the interest of saving a few trees (or bytes), the full listings for all of the various helper functions like valid_move aren’t in the book. If you want the full source for board.rs and game.rs, you can grab it from the book’s full example code. You’ll also see several unit tests in the full version of the code that shows how you can test this code without crossing the WebAssembly boundary.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.172.50