From 799b370e39207e58231c4bf49b067a4cb4da0198 Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Fri, 10 Aug 2018 13:25:57 -0400 Subject: [PATCH] Whole post instead --- ...-08-10-chess-and-recursion-part-1.markdown | 288 +++++++++++++++++- 1 file changed, 281 insertions(+), 7 deletions(-) diff --git a/_posts/2018-08-10-chess-and-recursion-part-1.markdown b/_posts/2018-08-10-chess-and-recursion-part-1.markdown index fdbae34..214cdc4 100644 --- a/_posts/2018-08-10-chess-and-recursion-part-1.markdown +++ b/_posts/2018-08-10-chess-and-recursion-part-1.markdown @@ -1,19 +1,293 @@ --- title: "Chess and Recursion: Part 1" +layout: post categories: +- coding +- development +- games +- elixir date: 2018-08-10 --- + +
{% picture full-width blog/chess-and-recursion-part-1/escher-chess.jpg alt="My custom keyboard" %}
-My post "[Chess and Recursion: Part -1](https://robots.thoughtbot.com/chess-and-recursion-part-1)" has gone live on -the thoughtbot blog! +I’ve been using my investment time at thoughtbot to build a multiplayer chess +game using Elixir and Phoenix in order to hone my skills in that area. One of +the trickiest and most fun parts of the project so far has been generating all +the possible moves for a player to make. -> I’ve been using my investment time at thoughtbot to build a multiplayer chess -> game using Elixir and Phoenix in order to hone my skills in that area. One of -> the trickiest and most fun parts of the project so far has been generating all -> the possible moves for a player to make. + + +### The board + +We will store the board as a map of pieces indexed by their position as a tuple. +This makes it easy to move pieces around the board by popping elements out of +the map and then adding them back in with a new index. + +```elixir +%{ + {0, 7} => %{type: "rook", colour: "black"}, + {1, 7} => %{type: "knight", colour: "black"}, + {2, 7} => %{type: "bishop", colour: "black"}, + {3, 7} => %{type: "queen", colour: "black"}, + {4, 7} => %{type: "king", colour: "black"}, + {5, 7} => %{type: "bishop", colour: "black"}, + {6, 7} => %{type: "knight", colour: "black"}, + {7, 7} => %{type: "rook", colour: "black"}, + + {0, 6} => %{type: "pawn", colour: "black"}, + {1, 6} => %{type: "pawn", colour: "black"}, + # rest of the pieces ... +} +``` + +### Linear moves + +We’ll start this journey with rooks as they have a relatively straightforward +movement profile. For now we will ignore blocking pieces which means that for +each direction we just need to traverse the board in one direction until we hit +the edge. + +Looping in Elixir is achieved through recursion. This may sound complex but has +some advantages as we will see. + +Here’s a function that will return all the available moves in one direction: + +```elixir +# lib/chess/moves/rook.ex + +defmodule Chess.Moves.Rook do + def moves_north(_board, {_file, 7}), do: [] + + def moves_north(board, {file, rank}) do + [{file, rank + 1} | moves_north(board, {file, rank + 1})] + end +end +``` + +

+ +

+ +Let's break this down piece by piece. + +We're using Elixir's pattern matching to define multiple functions here with the +same name. If you'd like to read more about Elixir's pattern matching I would +recommend [Pattern Matching in Elixir: Five Things to +Remember](https://blog.carbonfive.com/2017/10/19/pattern-matching-in-elixir-five-things-to-remember/) +by Anna Neyzberg + +The first function matches if the `rank` is 7. This means we've hit the top edge +of the board so we return an empty list to stop the recursion. + +```elixir +def moves_north(_board, {_file, 7}), do: [] +``` + +(The underscores in front of the variable names indicate that we can discard the +values as we don't need them in the function definition.) + +Lists in Elixir are represented internally as linked lists. They are represented +internally by pairs consisting of the head and the tail of the list. The `|` +operator allows us to match the head and tail of a lists or construct a new list +from a head and a tail. + +The next function returns a new list with the head containing the next square +north and the tail being the result of another call to this function: + +```elixir +def moves_north(board, {file, rank}) do + [{file, rank + 1} | moves_north(board, {file, rank + 1})] +end +``` + +Essentially we are adding `1` to the rank until it reaches `7`, then unwinding +the stack and returning the resulting list. If `rank` starts at say `4` (we’ll +set `file` to `0`) then we get this back from the recursive function: + +```elixir +[{0, 4} | [{0, 5} | [{0, 6} | [{0, 7}]]]] +``` + +Which is equivalent to: + +```elixir +[{0, 4}, {0, 5}, {0, 6}, {0, 7}] +``` + +And there we have our list of moves in one direction! + +We can create other recursive functions to handle moving south, east and west: + +```elixir +def moves_south(_board, {_file, 0}), do: [] +def moves_south(board, {file, rank}) do + [{file, rank - 1} | moves_south(board, {file, rank - 1})] +end + +def moves_east(_board, {7, _rank}), do: [] +def moves_east(board, {file, rank}) do + [{file + 1, rank} | moves_east(board, {file + 1, rank})] +end + +def moves_west(_board, {0, _rank}), do: [] +def moves_west(board, {file, rank}) do + [{file - 1, rank} | moves_west(board, {file - 1, rank})] +end +``` + +

+ +

+ +We can even start writing functions to handle moving in diagonal directions, for +bishops and queens: + +```elixir +def moves_northeast(_board, {7, _rank}), do: [] +def moves_northeast(_board, {_file, 7}), do: [] +def moves_northeast(board, {file, rank}) do + [{file + 1, rank + 1} | moves_northeast(board, {file + 1, rank + 1})] +end +``` + +We're writing a lot of functions now that look very similar, so let's figure out +a better way. + +Each of these functions is doing the same thing—moving in a straight line—just +in a different direction. We could pass an offset vector as a tuple instead of +hard coding it into each function, so if we want all the moves north we could +call it like this: + +```elixir +moves(board, {3, 4}, {0, 1}) # (board, position, vector) +``` + +The main body of the function looks like this: + +```elixir +def moves(board, {file, rank}, {fv, rv}) do + next_square = {file + fv, rank + rv} + [next_square | moves(board, next_square, {fv, rv})] +end +``` + +We need to match cases where we hit the edge of the board and stop the +recursion: + +```elixir +def moves(_board, {0, _rank}, {-1, _}), do: [] +def moves(_board, {_file, 0}, {_, -1}), do: [] +def moves(_board, {7, _rank}, {1, _}), do: [] +def moves(_board, {_file, 7}, {_, 1}), do: [] + +def moves(board, {file, rank}, {fv, rv}) do + next_square = {file + fv, rank + rv} + [next_square | moves(board, next_square, {fv, rv})] +end +``` + +### Obstructions + +We need to handle cases where another piece is in the way. First, let’s define +a function that will tell us if a square is empty: + +```elixir +# we'll use defp to define a private function as +# we won't be calling this outside of this module. +defp empty?(board, position) do + is_nil(board[position]) +end +``` + +Now we can use this function in our moves function to only recurse if the next +square is empty. If the square is not empty then we return an empty list to stop +recursion. + +```elixir +def moves(board, {file, rank}, {fv, rv}) do + next_square = {file + fv, rank + rv} + if empty?(board, next_square) do + [next_square | moves(board, next_square, {fv, rv})] + else + [] + end +end +``` + +

+ +

+ +### All together now + +Lastly, let’s combine these to generate moves for all the pieces that move in +straight lines: + +```elixir +defmodule Chess.Moves do + def queen(board, position) do + # The queen moves like both a rook and a bishop + rook(board, position) ++ + bishop(board, position) + end + + def rook(board, {file, rank}) do + moves(board, {file, rank}, {0, 1}) ++ + moves(board, {file, rank}, {0, -1}) ++ + moves(board, {file, rank}, {-1, 0}) ++ + moves(board, {file, rank}, {1, 0}) + end + + def bishop(board, {file, rank}) do + moves(board, {file, rank}, {1, 1}) ++ + moves(board, {file, rank}, {1, -1}) ++ + moves(board, {file, rank}, {-1, 1}) ++ + moves(board, {file, rank}, {-1, -1}) + end + + defp moves(_board, {0, _rank}, {-1, _}), do: [] + defp moves(_board, {_file, 0}, {_, -1}), do: [] + defp moves(_board, {7, _rank}, {1, _}), do: [] + defp moves(_board, {_file, 7}, {_, 1}), do: [] + defp moves(board, {file, rank}, {fv, rv}) do + next_square = {file + fv, rank + rv} + if empty?(board, next_square) do + [next_square | moves(board, next_square, {fv, rv})] + else + [] + end + end + + defp empty?(board, position) do + is_nil(board[position]) + end +end +``` + +

+ +

+

+ +

+ +That’s all for now, next time we’ll tackle the gnarly moves of the knight! + +If you’re impatient for more, you can always check out the source code on GitHub +at [https://github.com/danbee/chess](https://github.com/danbee/chess). + +*This post was also published to the [thoughtbot +blog](https://robots.thoughtbot.com/chess-and-recursion-part-1).*