(en) PyCheckers — #4 Moves and captures, multiple games, refractoring the code

in #utopian-io6 years ago (edited)

PyCheckers is series of articles in which I describe my process of creating a checkers board game (also known as English draughts) in Python programming language. This is a project for my studies and I still learn to code in Python, therefore I invite you all to follow up and learn with me.

PyCheckers series

PyCheckers series logo.



Background image by Paul Brennan (Public Domain). Python logo by Python Software Foundation.

Introduction

In the previous episode of PyCheckers series we have finally created a chess board filled with pieces grabbed from Python objects. We also allowed user to select a piece and implemented a fairly simple turns-mechanism, which we'll use today for a more sophisticated gameplay with moves and captures.

This post will be quite extensive, as we will not only allow pieces to be moved and captures, but we will also refractor the code of PyCheckers app, so it's easier to maintain the project and also implement future features.

Things that you'll learn:

  • Creating the optimal architecture for a Python app (or any object-oriented programming app, to be honest).
  • Handling advanced URL-routing in Flask framework.
  • Handling forms in Flask framework.
  • Using Flask's built-in flash messages system.
  • Working with Python's sets and dictionaries.
  • What is UUID and how to use it.

Requirements

You will need:

  • PyCheckers app with basic board and selectable pieces (see Part 3 of this series to follow up).
  • Basic knowledge of any programming language.
  • Very basic knowledge of object-oriented programming (or at least the idea).
  • Basic knowledge of HTML (+ HTML forms) and CSS.

Difficulty

You may find today's article hard to follow if you do not know how Python modules work, and also how classess and objects work in general in object-oriented-programming languages.

  • Intermediate

Contents

1. Refractoring the code

Last time we stored all our classes in a single file called Utils.py. This is not a good approach, so today we will start fixing that.

2. Allowing multiple games to be started

Up to now, we only had one built-in game running in our app. Let's allow multiple games to be started and played simultaneously!

3. Implementing moves

Last time we could select a piece, but now we will go deeply into Flask's routing system and allow pieces to be moved along the board.

4. Implementing captures

Our pieces can move, and they should also be able to capture enemy's pieces—let's implement that!


Refractoring the code

Remember the Utils.py file? In it we stored basically all of our classes that are required to make the app work. This resulted in a monstrous file:

"""
This is Utils module.
It stores basic functions that allow the game to work.
TODO: Prepare better architecture, i.e. store each object in its own file.
"""
class Board:
    """
    Stores the basic board setup, i.e. the dimensions.
    """
    columns = rows = 8
    turn = 0
    
    def __init__(self):
        self.columns = 8
        self.rows = 8
        self.turn = 0  # 0 for Black and 1 for White
    
    def get_rows(self):
        """
        Returns an iterable list of rows.
        :return list:
        """
        return range(self.rows)
    
    def get_columns(self):
        """
        Returns an iterable list of columns.
        :return list:
        """
        return range(self.columns)

    @staticmethod
    def check_if_allowed(row, column):
        if (row % 2 == 0 and column % 2 != 0) or (row % 2 != 0 and column % 2 == 0):
            return True
        else:
            return False

    def change_turn(self):
        if self.turn == 0:
            self.turn = 1
        elif self.turn == 1:
            self.turn = 0
        else:
            print('Error!')



class Player:
    """
    A basic object that stores data about a single player,
    i.e. where he has got his pieces and which of them are kings.
    """
    pieces = 12
    positions = set()
    kings = set()

    def __init__(self):
        self.pieces = 12


class PlayerWhite(Player):
    """
    Inherits from Player class.
    Stores data about a player playing white pieces.
    """
    positions = {
        (0, 1), (0, 3), (0, 5), (0, 7),
        (1, 0), (1, 2), (1, 4), (1, 6),
        (2, 1), (2, 3), (2, 5), (2, 7)
    }

    kings = set()


class PlayerBlack(Player):
    """
    Inherits from Player class.
    Stores data about a player playing black pieces.
    """
    positions = {
        (5, 0), (5, 2), (5, 4), (5, 6),
        (6, 1), (6, 3), (6, 5), (6, 7),
        (7, 0), (7, 2), (7, 4), (7, 6)
    }

    kings = set()

This already counts in 87 lines, and we're just in the beginning of our development process! That's now how things should be done.

A much better approach would be either:

  1. To store classes in separate files, depending on how they relate to each other. We could have a file Players.py with three classes inside: Player, PlayerWhite(Player), and PlayerBlack(Player).
  2. To store each class in its own file. This way both Player, PlayerWhite and PlayerBlack would have their own files named accordingly.

I believe that both of these approaches would work just as well in our case, but I've decided to use the second one.

One-class-per-file approach is very often recommended for developing apps in object-oriented programming languages and it surely works better for large-scale projects. Thus, we'll be using it even if it's a slight overkill—just to get acommodated with it and develop good programming habits.

As you can see, in our case the Board class has nothing to do with classes like PlayerWhite or PlayerBlack. In the next step we'll be also creating a new class called Game, which is more general idea and just doesn't fit with any of other classes.

So, let's get to work!

Creating /resources directory

We'll start by creating a new /resources directory in our app folder. Why? Because soon we'll have half-a-dozen of different files, and keeping them all in the root directory will not help code redability—and that's what we aim for.

Let's also create all the required files inside that directory and leave them empty for now. That's our app structure now:

run.py
/pycheckers
. . /resources
. . . . __init__.py 
. . . . Board.py
. . . . Game.py
. . . . Player.py
. . . . PlayerBlack.py
. . . . PlayerWhite.py
. . /templates
. . . . base.html
. . . . index.html
. . . . board.html
. . /static
. . . . __init__.py
. . views.py
. . utils.py

Let's break it up: We created a file for each class we have in our app, and also one additional file Game.py for the class called Game. We'll use it further into this post.

It's also important to note the __init__.py file inside the /resources directory. It is required for Python interpreter to know it is a module. As per Python documentation:

The __init__.py files are required to make Python treat the directories
as containing packages; this is done to prevent directories with a common name,
such as string, from unintentionally hiding valid modules that occur later
on the module search path. In the simplest case, __init__.py can just be
an empty file, but it can also execute initialization code for the package(...).

6. Modules, Python 3.6.4 documentation

In our case the file will just be empty, but it is required.

Moving classes to the new /resources directory

We're now ready to move our classes from utils.py file to their new files inside /resources directory. It's fairly simple, so I won't go into detail—just copy and paste each single class into a file with the name of this class.

After that we're almost good to go. Why I say almost? Well, by separating our classes we have unlinked some dependencies. For example, PlayerWhite(Player) class inherits from Player base class. But now this base class just isn't there, so trying to run our app won't work.

We have to import the Player class into PlayerWhite.py file, so we can use it. That's how PlayerWhite.py file looks like now:

class PlayerWhite(Player):
    #  All of the stuff here

Note that I have skipped the content of PlayerWhite(Player) class as it's not important right now.

To make it work again, we'll change this file like this:

from . import Player

class PlayerWhite(Player.Player):
    #  All of the stuff here

What is happening?

We firstly import our Player base class with: from . import Player. The dot (.) in this line means realtive import, so import from a package we are already in. We are in a package called Resources, and inside this package we have a package Player (indicated by the file Player.py).

So, we're basically telling Python: import Player package from the same folder.

Now we just have to fix PlayerWhite class. Previously, we could inherit Player class simply by using: class PlayerWhite(Player). Now it's more tricky, since Player keyword means the entire Player module. But such a module can contain many classes, so we have to refer to a specific class—Player in our case.

That's why we use Player.Player— this means, that we want to use class Player from the module of the same name.

We will have to do the same for all other classes. I am not pasting the code here for the sake of space, you can view the source of all files in Github reposition, which I linked at the bottom of this article. After completely emptying the utils.py file, just remove it—we no longer need it.

More on relative imports

Relative imports mechanism in Python is very interesting and useful. Let's look at it more deeply.

In our case, we also have other possibility. We could do the importing from above in different way:

from .Player import Player

class PlayerWhite(Player):
    #  All of the stuff here

The difference is that we now tell Python to directly import the Player class from Player module in the same directory (.Player means just that, a Player module in the current directory).

As you can see, we can now just do PlayerWhite(Player), because Player now means a class, and not a module.

The dot operator (.) is very powerful, it allows us to traverse through Python-app directory-structure. We can use the dot multiple times.

Let's say we have CurrentClass.py file:

from ..ModuleAbove import SomeClass

class CurrentClass(SomeClass):
    pass

This way we would import SomeClass from a module FolderAbove that is in a directory above current one in the file-tree:

/ModuleAbove
. . __init__.py
. . SomeClass.py
. . /ModuleBelow
. . . . __init__.py
. . . . CurrentClass.py

As the dot operator is handy, it is as dangerous. It's easy to make classes' dependencies really messy and unclear using it too much. We will only be using it to the level on one dot, so it's fine.

Allowing multiple games to be started

We have prepared a better architecture for our PyCheckers app. This will help us a lot in this next step—allowing multiple games to be started and played simultaneously. For that purpose, we will finally fill up our Game.py module.

Remember our views.py file? At the top of that file we had few lines of code:

# TODO: Move variables from below to a new Game object.
# TODO: Allow to create multiple instances of the Game object (store as objects in array).
board_initialized = utils.Board()
rows = board_initialized.get_rows()
columns = board_initialized.get_columns()

player_black = utils.PlayerBlack()
player_white = utils.PlayerWhite()

This was not a right way to do it, because this way we only have one instance of the game, and it's inside views.py file.

But, as the TODO comments say, we will now remove these lines from views.py, and after editing it slightly it will be all good.

Game class

Game is a class in which we will store all data about a game: name of the game, board, players, which turn it is and so on. This way, we will be able to create multiple instances of this class—each one will be a completely different and separate game!

We can also say, that in a way Game class manages all other classes. Why? Because inside a Game instance we will have a Board, PlayerWhite and PlayerBlack, and so on. This is in line with a logical architecture: game is a general idea, and things such as board or pieces are dependent on it.

This is how the Game class will look like for now:

from . import Board
from . import PlayerBlack
from . import PlayerWhite


class Game:
    def __init__(self, name):
        self.name = name
        self.turn = 0
        self.board = Board.Board()
        self.rows = self.board.get_rows()
        self.columns = self.board.get_columns()
        self.player_black = PlayerBlack.PlayerBlack()
        self.player_white = PlayerWhite.PlayerWhite()

    def change_turn(self):
        """
        This method changes the turn from 0 to 1 or from 1 to 0.
        It returns False if something's wrong and the turn is neither 0 or 1. Should not happen.
        :return boolean:
        """
        if self.turn == 0:
            self.turn = 1
            return True
        elif self.turn == 1:
            self.turn = 0
            return True
        return False

As you can see, at the very top we first include all our submodules that we will be using: Board, PlayerWhite and PlayerBlack.

Below we have our Game class. Please note, that we do not simply move the lines from views.py into the class itself, but we move them into the __init__() method. It is a so-called magical function, a constructor of the class.

What this means is that whenever we create a new instance of the Game object, the __init__() method gets executed and creates new attributes for the instance (like rows or player_black).

We can't just pass these attributes as class' attributes, because then they would be shared by all of the instances of class. Changing the value in one instance would also cause other instances to have that specific value changes. Using __init__() we ensure that those variables are instance-wide and not class-wide (these are mine terms).

Change turn

You surely have noticed the change_turn() method. It is strictly linked to the turn attribute of the Game class. In other words, a Game instance has got an inner state of turn: either 0 or 1. We need a way to change these values—to change a turn.

Theoritcally we could simply do this in our code:

single_game_instance = Game('a name')

if single_game_instance.turn == 0:
  single_game_instance.turn = 1
else:
  single_game_instance.turn = 0

But this is bad for many reasons, i.e.

Reason #1 — DRY rule

DRY stands for Don't Repeat Yourself. It means that we should write our code in a way that doesn't require us to repeat the same code many times in our app.

Imagine we wanted to change the turn after player moves a piece, or after he/she captures a piece. These are two different places in which we have to change the turn. It means we would have to repeat the code above twice. Now imagine, that we'll have 5-10 places in which we'll be wanting to change the turn. This gets messy quickly.

And now imagine, that you want to be using 'black' and 'white' string instead of integers... You'd have to change it everywhere where you have repeated the code.

By using a dedicated method, we just call single_game_instance.change_turn() and the method does all the job for us. If we wanted to use strings instead of integers, we would just edit the change_turn() method and that's all.

Reason #2 — Encapsulation

Encapsulation is the idea, that only the object (the instance) itself has access to its attributes. Any other objects, classes and functions cannot access its attributes directly. In some languages we even have so called access modifiers like: public, private and protected. In Python we don't have such modifiers, so we have to be cautious and keep an eye on our classes.

That's not to mention, that directly changing object's attributes is generally considered a very bad practice. By doing this, we have no control over object's internal state. We can only guess what's inside a given attribute—especially in languages like Python, where we have so called duck typing. It means that we do not have to declare variable type explicitly, like int number = 2. This means, that we could store virtually anything in our number variable: from integers, to strings, dictionaries, and even objects.

Importin everything to views.py

We now have everything read to finally implement multiple games feature. How we'll do it? Well, there are at least a few different ways to do so, but my idea is simple:

  1. Create games dictionary.
  2. Inside this dictionary store game instances, where:
    1. key is a unique game ID
    2. value for that key is the game object itself.

This way we have a central place where we store all of our games, and we can easily get access to all of them, or to a one single game—by its unique ID.

UUID

Having reached this point, I was thinking about the system I'd use for generating IDs for games. I decided to go with UUID (Universally Unique Identifier). It's probably one of the best solutions of its kind, and that is to generate unique identifiers that do not require any authority confirmation and are almost 100% unique. Almost, because there is a very slight change that two generated identifiers could be the same, but chance for such a situation are so small, that it's almost impossible.

An identifier generated with UUID looks like this: 6573940c-b48c-40ac-a934-c96d0faf645f. There are 5 versions of UUID, which generate the identifier from different sources. If you want to know more, you should read about Universally unique identifier at Wikipedia.

Importing everything

Let's go back to our views.py file.

Firstly, let's import all the required modules that we'll now be using.

import uuid
from pycheckers import app
from .resources import Game, Board
from flask import render_template, request, redirect, url_for, flash

What's new? We import uuid module—yes, it's used to generate UUID-identifiers in our app. Then we import app as usual, but let's see below:

from .resources import Game, Board—this part lets us import our newly created classes! Cool!

We also import two new modules from flask: request and flash. The first one we will use to handle forms, and the second one is for flash messages.

Let's also add our games dictionary below:

games = dict()
board = Board.Board()

We also add a board variable for temporary use, for example to check whether a selected piece is a valid one (and not outside the board dimensions).

Flash messages

The last thing I'll introduce here is Flask's flash messages. It's a handy system for displaying so called flash messages to the user. It's commonly used to inform the end-user about some errors or notifications.

In our case, we can use the system to show messages that the game has been created successfully, or that a form was filled in a wrong way (e.g. when trying to create a new game).

Flash messages work really simply. If we want to show a message to the user, we execute flash() function. It can take either one or two arguments. First one is the message content, and the other one is message category (e.g. success, error, notification etc.).

Example message may look like this in our code:

flash('Logged in successfully!', 'succes')

Flash messages won't work if we do not set up a special secret key for our app. It should be something random, we can for example generate a new UUID and use it. In my case, the secret key is this:

app.secret_key = 'C~\xb2\x95\x00:\xca\xc8b\x83\x89\xee\xf7)w&\xed\x96\xbe\x13\xfd\x88\x92\x81'

Creating new routes

Starting a new game

We'll start by creating two new routes for starting a new game:

@app.route('/start', methods=['POST', 'GET'])
def start():
    if request.method == 'POST':
        name = request.form['name']
        gametype = request.form['gametype']
        if name and gametype:
            return redirect(url_for('create', name=name))
        return render_template('start.html')


    @app.route('/create/<string:name>')
    def create(name):
        game_id = str(uuid.uuid4())
        games[game_id] = Game.Game(name)
        flash('Game started!', 'success')

        return redirect(url_for('game', game_id=game_id))

Let's break it up. We first define a new route at the /start address. On this subpage we will want to display a simple form for creating a new game. We just want a name for the game. If the form is filled, we then redirect to the /create/<string:name> route.

This second route exists solely for creating a new game instance. It firstly generates a new UUID identifier and saves it to a game_id variable. After that, we can create a new dictionary entry! We do it with games[game_id] = Game.Game(name), where game_id is our key, and a new Game.Game object is a value for that key.

We also use flash messages here to show user a success message, that the game was started successfuly. After that, we can simply redirect to the /game route (which is basically the /board route from before, just renamed). We must pass the game_id argument to this route in order to let Flask know which game extacly should it load.

Viewing active games

Let's also create a new subpage that will list all current games:

@app.route('/games')
    def active_games():
    return render_template('games.html', games=games)

Pretty straight-forward. We just render a new games.html template, and feed it with our entire games dictionary, that contains all game instances.

The games.html template looks like this:

{% extends "base.html" %}
{% block title %}Active games{% endblock %}
{% block content %}
    <div class="container">
        <h2>Active games</h2>
        <p class="large">Below you can see a list of all active games.</p>
        {% if games %}
            <div class="row gutters">
            {% for game in games %}
                <div class="col col-4">
                    <a class="game" href="{{ url_for('game', game_id=game) }}">
                        {% if games[game].gametype == 'human' %}
                            <span class="label black">Human vs. human</span>
                        {% else %}
                            <span class="label focus">Human vs. computer</span>
                        {% endif %}
                        <h3>{{ games[game].name }}</h3>
                    </a>
                </div>
            {% endfor %}
            </div>
        {% else %}
            <p>No games currently. Why don't you <a href="{{ url_for('start') }}">start one</a>?</p>
        {% endif %}
    </div>
{% endblock %}

As you can see, we iterate all games, and for each instance we create a new link leading to the /game/<string:game_id> route and feeding it with the right ID. Inside the link we display game's name and type.

Injecting variables

Remember our @app.context_processor? We used this handy tool to inject some variables automatically to all templates:

@app.context_processor
def inject_variables():
    """
    Injects variables into every view.
    Right now it is a very ugly workaround, but just for now.
    :return dict:
    """
    return dict(
        board={'rows': rows, 'columns': columns},
        player_black=player_black,
        player_white=player_white,
        turn=board_initialized.turn
    )

We will now extend its use massively. Firstly, let's take a look at it:

@app.context_processor
def inject_variables():
    """
    Injects variables into every view.
    :return dict:
    """
    rule = str(request.url_rule)
    if 'game/' in rule:
        s = request.url.split('/')
        game_id = s[s.index('game')+1]
        return dict(
            rule=rule,
            name=games[game_id].name,
            board={'rows': games[game_id].rows, 'columns': games[game_id].rows},
            player_black=games[game_id].player_black,
            player_white=games[game_id].player_white,
            turn=games[game_id].turn
        )
    else:
        return dict(
            rule=rule
        )

Firstly, let's talk about the rule. It's a variable we'll use to know where we are at the moment. And by that I mean that we will know in which route we currently are.

For that we grab the url_rule from request module we have just imported. It returns the route we define in @app.route() parts.

Let's say that we're at: example.com/game/6573940c-b48c-40ac-a934-c96d0faf645f. The requst.url_rule in that case will return: /game/<string:game_id>. So it does not return the real URL, just the general route.

We can use that, because we want to inject different data, depending whether user views a game or different subpages (like the one for starting a new game).

if 'game/' in rule

This is the important part. If this is true, it means that the viewed subpage is either the landing game page, or /select (when selecting a piece). In that case we must inject some variables, as you see.

But firstly, we must know which game exactly it is. The request module is helpful once again, as we can use the request.url to actually get the real URL address.

From that address we can grab the ID. We split the address into pieces, delimited by /. We end up with a list, such as: ['example.com', 'game', '6573940c-b48c-40ac-a934-c96d0faf645f']. It's as simple, as grabbing the element that's after the 'game' one.

We can now easily inject all the variables we'll use in our templates: player_white, player_black, turn and so on.

A /game route

As I mentioned, this is basically the same as /board route from previous articles, but just renamed so it makes more sense now.

@app.route('/game/<string:game_id>')
def game(game_id):
    """
    A basic view that just generates the board.
    :return render_template():
    """
    if games.get(game_id):
        return render_template('board.html', game_id=game_id)
    else:
        flash('Such a game does not exist. It might have ended.', 'error')
        return redirect(url_for('index'))

Also notice, that the URL address has changed from /board to /game/<string:game_id>. It's very important, because we now have to have our unique UUID in the URL address in order to identify which game instance we want to show and operate.

We also must take care of validating this, because someone could enter a URL address by hand, like: example.com/game/gotcha or just try to access a game that no longer exists. In that case, we first check whether there exists an entry in our games dictionary with given ID. We use .get() for this, because simply doing games[game_id] would throw an exception in case of non-existent key—and we do not want that!

It the test fails, it means that such a game does not exist. Maybe it was never there, maybe it had ended. We not only redirect user to the homepage, but we also show him/her a flash message.

Adding templates

We should now add the template for starting a new game:

{% extends "base.html" %}
{% block title %}Start{% endblock %}
{% block content %}
    <div class="container">
        <h2>Start a new game</h2>
        <form method="post" action="{{ url_for('start') }}" class="form">
            <div class="form-item">
                <label class="large strong">Name of the game <span class="req">*</span></label>
                <input class="big" type="text" name="name" required>
                <div class="desc">Enter the name of your game.</div>
            </div>
            <div class="form-item form-checkboxes">
                <p class="large strong">Who do you want to play against? <span class="req">*</span></p>
                <label class="checkbox"><input type="radio" name="gametype" value="computer" required disabled> Computer</label>
                <label class="checkbox"><input type="radio" name="gametype" value="human" required> Another player</label>
            </div>
            <div class="form-item">
                <button>Start a game</button>
                <a class="button secondary outline" href="{{ url_for('index') }}">Cancel</a>
            </div>
        </form>
    </div>
{% endblock %}

Implementing moves

We are ready to implement moves!

Updating selection route

Let's recall our /select route:

@app.route('/board/select/<int:coordinate_x>/<int:coordinate_y>')
def select(coordinate_x, coordinate_y):
    """
    A view that handles player selecting one of his pieces.
    :param int coordinate_x: This is a x coordinate of the piece that player selected.
    :param int coordinate_y: This is a y coordinate of the piece that player selected.
    :return render_template() or redirect():
    """

    if board_initialized.turn == 0 and (coordinate_x, coordinate_y) in player_black.positions\
    or board_initialized.turn == 1 and (coordinate_x, coordinate_y) in player_white.positions:
        return render_template('board.html', coordinate_x=coordinate_x, coordinate_y=coordinate_y)
    else:
        return redirect(url_for('board'))

Now it has to be changed in order to use the new architecture (i.e. instances):

@app.route('/game/<string:game_id>/select/<int:coordinate_x>/<int:coordinate_y>')
def select(game_id, coordinate_x, coordinate_y):
    """
    A view that handles player selecting one of his pieces.
    :param int coordinate_x: This is a x coordinate of the piece that player selected.
    :param int coordinate_y: This is a y coordinate of the piece that player selected.
    :return render_template() or redirect():
    """

    if games[game_id].turn == 0 and (coordinate_x, coordinate_y) in games[game_id].player_black.positions\
    or games[game_id].turn == 1 and (coordinate_x, coordinate_y) in games[game_id].player_white.positions:
        return render_template('board.html', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y)
    else:
        flash('You cannot select this piece!', 'error')
        return redirect(url_for('game', game_id=game_id))

Nothing changed in the logic itself, but we had to change the route from /board/select/<int:coordinate_x>/<int:coordinate_y> to /game/<string:game_id>/select/<int:coordinate_x>/<int:coordinate_y>. We must keep the game ID in URL address for our app to work. Thus the change.

In the body of the select() function itself, we use games[game_id] wherever we want to access the game instance. This instance has got attributes such as player_white, player_black and so on. These are actually PlayerWhite and PlayerWhite classes, so they have all their own attributes.

Route for move

Let's now add a completely new route for handling moves. move is an obvious choice and we'll go with that. There's one thing to consider, though.

We could now implement moves in two different ways. We could either remember coordinates of selected piece in our app, and use them for handling move. Then, the route would look like this: /game/<string:game_id>/move/<int:coordinate_x>/<int:coordinate_y>.

I, however, decided to just stack the routes in the URL address, so it's more clear and public. Therefore, we will still have to have /select route in our address, and then we will append the /move part. This will result in a quite long URL address, like: /game/6573940c-b48c-40ac-a934-c96d0faf645f/select/0/1/move/1/2. This is very clear in my opinion: select piece (0, 1) and move it to the box (1, 2).

This is how the definition of this route will look like:

@app.route('/game/<string:game_id>/select/<int:coordinate_x>/<int:coordinate_y>/move/<int:to_x>/<int:to_y>')
def move(game_id, coordinate_x, coordinate_y, to_x, to_y):
    """
    A view that handles moving a piece from one box to another.
    :param int coordinate_x: This is a x coordinate of the piece we want to move.
    :param int coordinate_y: This is a y coordinate of the piece we want to move.
    :param int to_x: This is a x coordinate of the box to which we want to move the piece.
    :param int to_y: This is a y coordinate of the box to which we want to move the piece.
    :return redirect():
    """

As you can see, we basically copied the route for selection, and appended the second part: /move/<int:to_x>/<int:to_y>. I think that these names are very easy to understand: (to_x, to_y) are coordinates of the box we want to move into.

That's nice, but how can we code the ability to move?

Checking moves

Handling a move is a two-step process. Firstly, we have to check whether the move is a valid one. Only then can we apply the changes (i.e. move the piece).

There are actually quite a few things we have to consider – some of them obvious, some not. A piece can't move outside the board (of course), it can't go into a white box (only gray ones). A piece can't also go to a box that is already occupied by a different piece. A piece also can't move backwards if it has not been turned into a king before.

And last but not least, only a piece that belongs to a player who's turn it is can move.

# Checking whether move is allowed
# 1. Not outside the board and onto other player's pieces
if to_x not in board.get_rows() or to_y not in board.get_columns() \
  or (to_x, to_y) in games[game_id].player_white.positions or (to_x, to_y) in games[game_id].player_black.positions:
    flash('This move is invalid!', 'error')
    return redirect(url_for('select', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y))

# 2. Only into gray boxes
if not board.check_if_allowed(to_x, to_y):
    flash('This move is invalid!', 'error')
    return redirect(url_for('select', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y))

# 3. Moving an existing piece
if (coordinate_x, coordinate_y) not in games[game_id].player_black.positions \
  and (coordinate_x, coordinate_y) not in games[game_id].player_white.positions:
    flash('This move is invalid!', 'error')
    return redirect(url_for('select', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y))

# 4. If not kings, pieces cannot move backwards
if (games[game_id].turn == 0 and (coordinate_x, coordinate_y) not in games[game_id].player_black.kings
    and coordinate_x <= to_x)\
  or (games[game_id].turn == 1 and (coordinate_x, coordinate_y) not in games[game_id].player_white.kings
    and coordinate_x >= to_x):
    flash('This move is invalid!', 'error')
    return redirect(url_for('select', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y))
1. Not outside the board and not onto other player's pieces

This checks for two things: whether (to_x, to_y) pair is in board.get_rows() and board.get_columns(). Remember these two methods? They return a list with numbers of all rows and columns, e.g.: [0, 1, 2, 3, 4, 5, 6, 7].

This bit of code simply checks whether the coordinate is in that list. If it's not, it must either be a negative number, or a number larger than 7. Either way, it means that such a box would be out of the board—thus it is invalid.

We also check, whether (to_x, to_y) is not already occupied by either White or Black Player, because in that case we can't move there.

2. Only into gray boxes

This is fairly simple. In checkers we can only move into so called dark boxes. We check that by invoking check_if_allowed() method from Board class instance. Let's see that method:

@staticmethod
def check_if_allowed(row, column):
    if (row % 2 == 0 and column % 2 != 0) or (row % 2 != 0 and column % 2 == 0):
        return True
    else:
        return False

It gets row and column numbers as arguments and simply checks whether these are odd or even numbers. Logic here is simple: if row is an even number, then the column must be odd and vice versa. This a simple recipe to check whether a box is gray or white.

The function returns True for a box that is valid to be moved into, and False ortherwise.

Let's also talk about @staticmethod. A static method, as opposed to ordinary methods, doesn't take the self argument, and it can take no arguments at all. Moreover, we can use that method even without creating any Board class instances!

It's basically an ordinary function, called syntactically like a method, but without access to the object and its internals.

3. Moving an existing piece

We also make sure that a piece that user tries to move does actually exist. For that purpose we check if (coordinate_x, coordinate_y) exists in either black's or white's player positions. If it does not, then such a piece does not exist and thus a move is invalid. We can redirect user back to the game, so he can select a piece again.


Note that I have mistakenly redirected user to the /select route with the wrong piece already selected. This is not a usability problem, as the /select route will validate this selection, and after finding out it's wrong, it will redirect back to the /game. But this is one unecessary redirection, and it would be wise to fix it for the better performance.

5. If not kings, pieces cannot move backwards

As in the title, pieces that are not kings can only move forward, i.e. away (in terms of rows) from their starting position.

It's easy to check this, as the rows are numbered from 0 (at the top) to 7 (at the very bottom). Since White Player starts at the top, and the Black Player starts at the bottom, therefore it's easy to see the pattern: if a white piece moves in such a way, that it's coordinate_x gets bigger (e.g. from 0 to 1), then it moves forward. It's exactly the opposite for black pieces.

Applying moves

We have all the basic checks done. If the moves-to-be-done passes through them all, we can check for the last one thing: by how many boxes a piece tries to move.

There are three cases: it can either move be one box (a simple move), by two boxes (only valid if it's a capture), and by more than two (always invalid).

Depending on which case it is, we'll want to do different things. We'll also take into accout, that every of these cases can happen for either black or white player.

Firstly, we'll create an outer check to see, whether it's black's or white's move.

if games[game_id].turn == 0 and (coordinate_x, coordinate_y) in games[game_id].player_black.positions:
    pass
elif games[game_id].turn == 1 and (coordinate_x, coordinate_y) in games[game_id].player_white.positions:
    pass

As you can see, if it's Black Player's turn and the selected piece belongs to him, we can start doing something with it. Otherwise, we check if it's White's turn and the piece belongs to him.

1. Normal move

In this case, a piece only moves by one square diagonally. How to know it? It's simple! We can use the absolute value (also known as modulus) function. This way we can simply calculate the distance of the move, and get the modulus out of it. This way we always get a positive number that we can simply check.

For that purpose we have to import math library into our views.py file. This library lets us use the fabs() function, that simply returns an absolute value of a given number.

Now onto the code:

# 1. If by one, it's okay—a normal move
if math.fabs(coordinate_x - to_x) <= 1 and math.fabs(coordinate_y - to_y) <= 1:
    games[game_id].player_black.positions.remove((coordinate_x, coordinate_y))
    games[game_id].player_black.positions.add((to_x, to_y))
    if (coordinate_x, coordinate_y) in games[game_id].player_black.kings:
        games[game_id].player_black.kings.remove((coordinate_x, coordinate_y))
        games[game_id].player_black.kings.add((to_x, to_y))
    games[game_id].change_turn()

So, if the absolute value of the distance betwen (coordinate_x, coordinate_y) and (to_x, to_y) is (1, 1) or less (this shouldn't ever happen, though), then the move is a valid move.

Now recall how we store our pieces. It's a set, with pieces stored there as pairs of coordinates. To move a piece, we must remove it and add it again, now with its new coordinates that it got after the move.

That's why we do player_black.positions.remove((coordinate_x, coordinate_y)) and then player_black.positions.add((to_x, to_y)). Note the double brackets—we want to add a tuple (to_x, to_y), and not both of these coordinates separately.

We also take are of kings, because kings is a separate set with its own set of coordinates. If the piece we want to move is a king, we must also update its coordinates there.

After all these operations, we can finally change the turn, so now the opposite player can make his move. We do it by invoking the change_turn() method on the game instance.

The code above only works for the Black Player, but it'll be just the same for the White one. The only difference will be that we will operate on player_white.positions instead of black's.

2. Captures

As I mentioned, another possibility is that a piece moves by two boxes diagonally. Such a move is only valid if it's a capture.

# 2. If by two, then check if it is a valid capture
elif math.fabs(coordinate_x - to_x) <= 2 and math.fabs(coordinate_y - to_y) <= 2:
    pass

We again calculate the modulus of the move, and if it's equal to (or smaller than) 2, it means we have to check whether it's a valid capture. Let's skip this part for now with a simple pass keyword.

3. Move by more than two boxes

We now have and if statement that works for moves by one box, another elif statement for moves by two boxes. All other options are invalid, so we can use else statement to safely treat all other possibilities.

There we flash a message, that informs the user that his move was invalid and he/she should try another one, and we redirect back to the game.

if games[game_id].turn == 0 and (coordinate_x, coordinate_y) in games[game_id].player_black.positions:
    # 1. If by one, it's okay
    if math.fabs(coordinate_x - to_x) <= 1 and math.fabs(coordinate_y - to_y) <= 1:
        #  Our code here
    # 2. If by two, then check if it is a valid capture
    elif math.fabs(coordinate_x - to_x) <= 2 and math.fabs(coordinate_y - to_y) <= 2
        pass
    # 3. If by more than two, then it's surely invalid
    else:
        flash('This move is invalid!', 'error')
        return redirect(url_for('select', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y))

That's all. Now, let's take care of this middle part: captures.

Implementing captures

We now know, that if a piece moves by two squares, the move is only valid if it's a capture. But how can we validate it? If a piece moves by two boxes, we could simply check if there's enemy's piece in the box in the middle (i.e. in the box that the piece jumps over).

Let's anaylize the situation presented below:

Possible capture

It's Black Player's turn, and he has got a piece (indicated by the white ring) that can capture a white piece. Gray box with a blue dot shows, where this black piece could move in order to capture the white piece.

Let's take a look at their coordinates:

Black's piece: (5, 0)
Gray box to move into: (3, 2)
White's piece: (4, 1)

Have you spotted a pattern here? The coordinates of the white piece is an average between the initial black piece position and the position it want's to go into. And that will always be right: when moving by two squares, we can get the average of the initial and final square, and that will give us the coordinates of the box in between.

This gives us a nice formula:

x_in_between = (old_x + new_x) / 2
y_in_between = (old_y + new_y) / 2

position_in_between = (x_in_between, y_in_between)

We cen now utilize it in our code:

elif math.fabs(coordinate_x - to_x) <= 2 or math.fabs(coordinate_y - to_y) <= 2:
    med_x = (coordinate_x + to_x) / 2
    med_y = (coordinate_y + to_y) / 2
    if (med_x, med_y) in games[game_id].player_white.positions:
        games[game_id].player_black.positions.remove((coordinate_x, coordinate_y))
        games[game_id].player_black.positions.add((to_x, to_y))
        if (coordinate_x, coordinate_y) in games[game_id].player_black.kings:
            games[game_id].player_black.kings.remove((coordinate_x, coordinate_y))
            games[game_id].player_black.kings.add((to_x, to_y))
        games[game_id].player_white.positions.remove((med_x, med_y))
        games[game_id].player_white.pieces -= 1
        if (med_x, med_y) in games[game_id].player_white.kings:
            games[game_id].player_white.kings.remove((med_x, med_y))
        games[game_id].change_turn()
        flash('A piece was captured!', 'black')
    else:
        flash('This move is invalid!', 'error')
        return redirect(url_for('select', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y))

As you can see, we're using med_x and med_y instead of x_in_between and y_in_between. Also, old_x and new_x are called differently. But it's the same formula.

So, after calculating the possition in between, we check whether enemy has a piece there. If yes, then such a move is a capture.

In a single move case, we were simply removing the moved piece, and adding it again with new coordinates. Here we do just the same, but we also remove the enemy's piece from his positions and kings (if the captured piece was a king) sets. We also decrease enemy's pieces variable by one. This is a variable that stores information on how many pieces left a player has got on the board.

It's here that we can safely change the turn, because the player has made his move. In fact it will be more complicated in the future, because we should not end the turn if this piece can make another capture. But let's not dig into that for now.

Finally, we show a flash message informing the user that he managed to capture enemy's piece.

We also took care of the situation, when there's no enemy's piece in the box in between. We just redirect back to the selection.

Again, the code above only handles black player. The code for white player is almost the same, therefore I am not including it in this article. You can view all the source files in the Github repository of PyCheckers game.

Updating the board.html template

That's quite a lot of changes we have made! Let's now update our board.html template. Last time it was fairly simple:

<table class="board unstyled">
    {% for row in board.rows %}
        <tr id="row-{{ row }}">
        {% for column in board.columns %}
            <td id="box-{{ row }},{{ column }}">
                {% if (row, column) in player_white.positions %}
                    {% if turn == 1 %}
                        <a class="player2 piece {% if (row, column) in player_white.kings %}piece--king{% endif %} {% if row == coordinate_x and column == coordinate_y %}piece--selected{% endif %}" href="{{ url_for('select', coordinate_x=row, coordinate_y=column) }}"></a>
                    {% elif turn == 0 %}
                        <div class="player2 piece {% if (row, column) in player_white.kings %}piece--king{% endif %}"></div>
                    {% endif %}
                {% elif (row, column) in player_black.positions %}
                    {% if turn == 0 %}
                        <a class="player1 piece {% if (row, column) in player_black.kings %}piece--king{% endif %} {% if row == coordinate_x and column == coordinate_y %}piece--selected{% endif %}" href="{{ url_for('select', coordinate_x=row, coordinate_y=column) }}"></a>
                    {% elif turn == 1 %}
                        <div class="player1 piece {% if (row, column) in player_black.kings %}piece--king{% endif %}"></div>
                    {% endif %}
                {% endif %}
            </td>
        {% endfor %}
        </tr>
    {% endfor %}
</table>

We were simply generating the board, and the only dynamic thing was that either black or white pieces were clickable links—depending on who's turn it was.

Our users could already select a piece, but now we must let them move them with not more than a single mouse button click.

That's the entirety of our new board.html file:

{% extends "base.html" %}
{% block title %}Board demo{% endblock %}
{% block content %}
    <div class="container">
        <h2>{{ name|escape }}</h2>

        <div class="message" data-component="message">
            {% if turn == 0 %}Black's{% elif turn == 1%}White's{% endif %} turn.
        </div>

        <table class="board unstyled">
            {% for row in board.rows %}
                <tr id="row-{{ row }}">
                {% for column in board.columns %}
                    <td id="box-{{ row }},{{ column }}">
                        {% if (row, column) in player_white.positions %}
                            {% if turn == 1 %}
                                <a class="player2 piece {% if (row, column) in player_white.kings %}piece--king{% endif %} {% if row == coordinate_x and column == coordinate_y %}piece--selected{% endif %}" href="{{ url_for('select', game_id=game_id, coordinate_x=row, coordinate_y=column) }}"></a>
                            {% elif turn == 0 %}
                                <div class="player2 piece {% if (row, column) in player_white.kings %}piece--king{% endif %}"></div>
                            {% endif %}
                        {% elif (row, column) in player_black.positions %}
                            {% if turn == 0 %}
                                <a class="player1 piece {% if (row, column) in player_black.kings %}piece--king{% endif %} {% if row == coordinate_x and column == coordinate_y %}piece--selected{% endif %}" href="{{ url_for('select', game_id=game_id, coordinate_x=row, coordinate_y=column) }}"></a>
                            {% elif turn == 1 %}
                                <div class="player1 piece {% if (row, column) in player_black.kings %}piece--king{% endif %}"></div>
                            {% endif %}
                        {% elif rule == '/game/<string:game_id>/select/<int:coordinate_x>/<int:coordinate_y>' and (row, column) not in player_white.positions and (row, column) not in player_black.positions %}
                            {% if (row is even and column is odd) or (row is odd and column is even) %}
                                <a class="box box--moveable" href="{{ url_for('move', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y, to_x=row, to_y=column) }}"></a>
                            {% endif %}
                        {% endif %}
                    </td>
                {% endfor %}
                </tr>
            {% endfor %}
        </table>

        <div class="row gutters between u-margin-top">
            <div class="col col-6">
                <h3>White loses</h3>
                {% if player_white.pieces < 12 %}
                    <ul class="loses loses--white">
                        {% for piece in range(player_white.pieces, 12) %}
                            <li class="loses__lost">
                                <div class="lost-piece"></div>
                            </li>
                        {% endfor %}
                    </ul>
                {% else %}
                    <p>No loses!</p>
                {% endif %}
            </div>
            <div class="col col-6 u-text-right">
                <h3>Black loses</h3>
                {% if player_black.pieces < 12 %}
                    <ul class="loses loses--black">
                        {% for piece in range(player_black.pieces, 12) %}
                            <li class="loses__lost">
                                <div class="lost-piece"></div>
                            </li>
                        {% endfor %}
                    </ul>
                {% else %}
                    <p>No loses!</p>
                {% endif %}
            </div>
        </div>

    </div>
{% endblock %}

As you can see, we added few things to make it look nicer: we display a name of the game, information on who's turn it is. Below the board we also show dynamically generated statistics, showing how many pieces each player has lost so far.

However, we were rendering all unoccupied boxes as an empty cells. Now, we will want to render links inside them, so users can select a piece and then click an empty box to move the piece there.

What really makes this work, is this part:

{% elif rule == '/game/<string:game_id>/select/<int:coordinate_x>/<int:coordinate_y>' and (row, column) not in player_white.positions and (row, column) not in player_black.positions %}
    {% if (row is even and column is odd) or (row is odd and column is even) %}
        <a class="box box--moveable" href="{{ url_for('move', game_id=game_id, coordinate_x=coordinate_x, coordinate_y=coordinate_y, to_x=row, to_y=column) }}"></a>
    {% endif %}
{% endif %}

We firstly check the actual route address. We don't want to make empty boxes clickable when a piece is not selected. That's why we do it only when the url rule is /game/<string:game_id>/select/<int:coordinate_x>/<int:coordinate_y>.

Inside we also check, whether the box is a gray one, so a piece can be moved into it. (row is even and column is odd) or (row is odd and column is even) works just as our check_if_allowed() method from before. And yes, we could have used it, but I forgot about it. I'll fix that in the upcoming episodes, because we still have a lot of refreactoring and optimization to do.

If the box is gray, we create there a link, that leads to the /move route, feeding it with four arguments: coordinate_x and coordinate_y come from the currently selected piece, while to_x and to_y are simply the coordinates of the box that the link is generated in.


Summing it up

Today we took care of a basic refractorization of our code, so that it's easier to maintain and to add new features in the future. We will now have our app divided into logical set of modules which work together.

Also, multiple games can now be started and be played simultaneously. This is not a big deal yet, but once we implement users it will be huge to allow them to create many games and play with each other.

About the game itself, we allowed pieces to be moved along the board. We also took care of validating those moves, so that a piece can only move forward (and backward if it's a king) by one square diagonally.

We then implemented captures: a conditional move by two squares, that is valid if there's an enemy's piece in the mid-position. This means that we now have the very basic game mechanisms ready: selecting, moving, capturing.

All the progress that we have made paves the road to creating more sophisticated rules, like forcing captures when they are possible, or allowing so called multiple captures.

Next time we'll also further extend Game object to allow creating private and public games, we'll implement users to our system, and these users will be able to create their own games and invite people to play with them!

Github repository

If you'd like to browse through the code of PyCheckers, you're free to do it on its Github repository. Below I link the entire repo, which is ahead of this series, and also a specific commit that is up to this article. You should browse through it in order to see the full picture.

Github repository:
mciszczon/PyCheckers

Commit that is up-to-date with this post:
b8be3bca7fa02124a82f45c1c91ce25be988d5bf

I encourage you to follow my PyCheckers series if you're interested in technology, programming and artificial intelligence or any similar field. PyCheckers will get into algorithmic thinking and learn some new programming skills.

Also, feel free to comment articles in this series. I am not a professional programmer, so there for sure will be some bugs, mistakes and things that could be done better. If you spot anything like that, have got something else to add or want to ask some questions—feel free to do it!

Thank you for your attention and see you next time!


PyCheckers Series (Curriculum)

This is the third post in the PyCherckers series, in which I write about an English-draughts-game I develop in Python 3 and Flask web-framework for my classess at university. Below you can find links to previous posts:

#1 — Introduction (on Steemit)

Here I talk about the origin of the project, why I do it, how I plan to do it and so on. I also describe English draugts rules extensively for those who do not know them.

#2 — Creating a base application

In this part I write about setting up an environment in which I'll be creating my game. I also show how to create a very basic web application in a Flask framework, which I'll be using.

#3 — Dynamic board with turns and selectable pieces

In the third part I describe Board and Player (and its children) classes, how to generate a dynamic board with pieces on it. Then I implement a simple turns-mechanism and allow a piece to be selected.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Hey @mciszczon I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

@mciszczon, I like your contribution to open source project, so I upvote to support you.

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Coin Marketplace

STEEM 0.21
TRX 0.13
JST 0.029
BTC 67557.12
ETH 3500.56
USDT 1.00
SBD 2.70