Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / DirectX

DirectX Board Game Engine

4.64/5 (19 votes)
29 Nov 2007CPOL11 min read 4   1.7K  
An article on how to create a generic engine for board games such as Checkers or Chess

Example using Checkers logic

Introduction

This article attempts to describe a way of creating a set of classes that are generic enough to allow several kinds of board games to be easily implemented. Only the actual game logic (the rules of the game) should have to change between implementations.

Updated

This article have been updated with two things:

  • General cleanup of the API, including a generic System.Windows.Forms.Form implementation that further reduces implementation time
  • An implementation of Connect Four

Both of these additions are discussed further in this article.

Requirements

When starting this project, I settled for a set of requirements that the finished code needed to fulfill:

  1. The implementation must be generic enough to allow an implementation of Checkers and an implementation of Chess to be created without modifying the board game implementation.
  2. Visual representation of the game must be in 3D using DirectX.
  3. When implementing a new board game, the person implementing does not need to have any knowledge of 3D mathematics or Direct3D.
  4. The implementation should not require a "game loop," as this is often an unfamiliar concept to persons not used to implementing games.
  5. A System.Window.Forms.Panel component should be used to render the game so that it can be placed onto a form like any other visual component.

Using the Code

The Visual Studio solution is made up of two projects:

  • A class library containing the generic code that should be applicable to any board game (I will refer to this as "the Framework")
  • A Windows application demonstrating the board game library using an implementation of Checkers

The game logic (in this example, the Checkers implementation) must implement an interface called IBoardGameLogic, which is defined as:

C#
using System;
using System.Collections.Generic;

namespace Bornander.Games.BoardGame
{
    public interface IBoardGameLogic
    {
        /// <summary>
        /// Number of rows on the board.
        /// </summary>

        int Rows
        {
            get;
        }

        /// <summary>
        /// Number of columns on the board.
        /// </summary>

        int Columns
        {
            get;
        }

        /// <summary>
        /// Returns the state (the piece) at a specific row and column.
        /// </summary>

        /// The row on the board (0 based).
        /// The column on the board (0 based).
        /// <returns>A value indicating the 

        /// piece that occupies this row and column.
        /// For empty squares zero should be returned.</returns />

        int this[int row, int column]
        {
            get;
            set;
        }

        /// <summary>
        /// This method returns the same as int this[int row, int column].
        /// </summary>

        int this[Square square]
        {
            get;
            set;
        }

        /// <summary>
        /// Return the currently available moves.
        /// </summary>

        List<move /> GetAvailableMoves();

        /// <summary>
        /// Initializes the game to its start state.
        /// </summary>

        void Initialize();

        /// <summary>
        /// Used to determine if the game is over.
        /// </summary>

        /// A string describing the game
        /// over state if the game is over, example "Player one lost!".
        /// <returns>True if the game is over.</returns>

        bool IsGameOver(out string gameOverStatement);

        /// <summary>
        /// Moves a piece on the board.
        /// </summary>

        /// From square.
        /// To square.
        /// <returns>The available moves after
        /// the move was made.</returns>

        List<move> Move(Square square, Square allowedSquare);
    }
}

By exposing these few methods, the Framework can control the game flow and make sure that the rules of the game are followed. There is one thing missing, though: there is no way for the Framework to figure out what to display. It can deduce a state of each square, but it has no information about what should be rendered to the screen to visualize that state. This is solved by providing the Framework with an instance of another interface, called IBordGameModelRepository:

C#
using System;
using Bornander.Games.Direct3D;

namespace Bornander.Games.BoardGame
{
    public interface IBoardGameModelRepository
    {
        /// <summary>
        /// Initializes the Models.
        /// </summary>

        void Initialize(Microsoft.DirectX.Direct3D.Device device);

        /// <summary>
        /// Returns the visual representation for the board square
        /// at the location given by the Square.
        /// This is for example either a black box or a white box for
        /// a chess implementation.
        /// </summary>

        Model GetBoardSquareModel(Square square);

        /// <summary>
        /// Returns the visual representation for a specific id.
        /// This is for example the 3D model of a pawn in a chess
        /// implementation.
        /// </summary>

        Model GetBoardPieceModel(int pieceId);
    }
}

By keeping the interfaces IBoardGameLogic and IBoardGameModelRepository separate, we allow the game logic to be completely decoupled from the visual representation. This is important because we might want to port this game to a Windows Mobile device, for example, where a 2D representation is preferred over a 3D one.

Now that the Framework has access to all of the information it needs for rendering the state of the game, it is time to consider the render implementation. Almost all game rendering is handled by VisualBoard. This class queries IBoardGameLogic and uses the information returned, together with the Models returned by IBoardGameModelRepository, to render both the board and the pieces.

There is one element that is not rendered by VisualBoard and that is the currently selected piece, i.e., the piece the user is currently moving around. Another class called GamePanel, which extends System.Windows.Forms.Panel, handles input as well as selecting and moving pieces around on the board. This type of implementation might seem to lower the inner cohesion of the GamePanel class, but I decided to do it this way because I want VisualBoard to render the state of the board game. That state does not know anything about a piece currently being moved.

Classes Overview

Board Game Class Library

These are the classes in the class library:

  • GamePanel extends System.Windows.Forms.Panel and handles user input, DirectX setup and some rendering
  • VisualBoard is responsible for rendering IBoardGameLogic
  • IBoardGameLogic, an interface, represents the rules of a board game
  • IBoardGameModelRepository, an interface, represents a repository of 3D models used to render IBoardGameLogic
  • Move represents all possible moves originating from a specific Square
  • Square represents a board square by holding row and column information
  • Camera represents a camera that is used when rendering in 3D
  • Model groups a Mesh, a Material and a position and orientation together for convenience

Checkers Application

These are the classes in the Checkers application:

  • CheckersModelRepository implements IBoardGameModelRepository and is responsible for returning models related to rendering the Checkers board
  • CheckersLogic is the Checkers game logic implementation; it implements CheckersLogic

Rendering the Game State

Rendering the state of the game is pretty straightforward: loop over all board squares, render the square and then render any piece occupying that square. Simple. However, we also need to indicate to the user which moves are valid. This is done by highlighting the board square under the mouse if it is valid to move from that square (or to that square when "holding" a piece).

It is important to mention that the Framework makes the assumption that the squares are 1 unit wide and 1 unit deep (height is up to the game developer to decide). This must be taken into account when creating the meshes for the game. To help out with this, the Model class holds two materials: one "normal" material and one "highlighted" material.

Allowed square is highlighted when mouse is over

By checking whether the model is in state "Selected," it sets its material to either normal or highlighted just prior to rendering, like this:

C#
/// <summary>
/// Array containing two materials, at 0 the normal material 
/// and at 1 the highlighted or selected material.
/// </summary>

class Model
{
    private Material[] material;

    ...

    public void Render(Device device)
    {
        device.Transform.World = this.World;
        device.Material = material[selected ? 1 : 0];
        mesh.DrawSubset(0);
    }
}

VisualBoard simply sets the selected state for each board square model before rendering it in its Render method:

C#
public class VisualBoard
{
        ...

    public void Render(Device device)
    {
        for (int row = 0; row < gameLogic.Rows; ++row)
        {
            for (int column = 0; column < gameLogic.Columns; ++column)
            {
                Square currentSquare = new Square(row, column);

                Model boardSquare =
                    boardGameModelRepository.GetBoardSquareModel
                    (currentSquare);
                boardSquare.Position = 
                    new Vector3((float)column, 0.0f, (float)row);
                boardSquare.Selected = currentSquare.Equals(selectedSquare);
                boardSquare.Render(device);

                // Check that the current piece isn't grabbed by the mouse,
                // because in that case we don't render it.
                if (!currentPieceOrigin.Equals(currentSquare))
                {
                    // Check which kind of model we need to render,
                    // move our "template" to the
                    // right position and render it there.
                    Model pieceModel = 
                        boardGameModelRepository.GetBoardPieceModel
                                        (gameLogic[currentSquare]);
                    if (pieceModel != null)
                    {
                        pieceModel.Position = new Vector3((float)column,
                                                    0.0f, (float)row);
                        pieceModel.Render(device);
                    }
                }
            }
        }
    }
}

Figuring out which square is actually selected is a matter of finding which square is "under" the mouse. In 2D, this is a really simple operation, but it gets slightly more complicated in 3D. We need to grab the mouse coordinates on the screen and, using the Projection and View matrices, un-project the screen coordinates to 3D coordinates. Then, when we have our mouse position as a 3D position, we can cast a ray towards all our board square Models to see if we get an intersection. The code for this can be difficult to understand for someone not used to 3D mathematics. This is why the Framework must take care of it for us so that we (the guy or gal implementing a board game) don't have to worry about such things. A function handles all this in the VisualBoard class:

C#
/// <summary>
/// This method allows to check for "MouseOver" on Models in 3D space.
/// </summary>

public bool GetMouseOverBlockModel(Device device, int mouseX, int mouseY, 
    out Square square, List<square /> highlightIfHit)
{
    selectedSquare = Square.Negative;
    square = new Square();
    bool foundMatch = false;
    float closestMatch = int.MaxValue;
    for (int row = 0; row < gameLogic.Rows; ++row)
    {
        for (int column = 0; column < gameLogic.Columns; ++column)
        {
            Square currentSquare = new Square(row, column);
            Model boardSquare = 
                boardGameModelRepository.GetBoardSquareModel(currentSquare);
            boardSquare.Position = 
                new Vector3((float)column, 0.0f, (float)row);

            Vector3 near = new Vector3(mouseX, mouseY, 0.0f);
            Vector3 far = new Vector3(mouseX, mouseY, 1.0f);

            // Unproject a vector from the screen X,Y space into 
            // 3D space using the World matrix of the mesh we are checking.
            near.Unproject(device.Viewport, device.Transform.Projection, 
                device.Transform.View, boardSquare.World);
            far.Unproject(device.Viewport, device.Transform.Projection, 
                device.Transform.View, boardSquare.World);
            far.Subtract(near);

            // Find the closes match of all blocks, that is the one 
            // the mouse is over.
            IntersectInformation closestIntersection;
            if (boardSquare.Mesh.Intersect(near, far, 
                 out closestIntersection)
                 && closestIntersection.Dist < closestMatch)
            {
                closestMatch = closestIntersection.Dist;
                square = new Square(row, column);

                // If a list of squares is passed in we are over 
                // one of those squares we highlight that
                // this is used to indicated valid moves to the user
                if (highlightIfHit != null)
                {
                    foreach (Square highlightSquare in highlightIfHit)
                    {
                        if (highlightSquare.Equals(square))
                        {
                            selectedSquare = new Square(row, column);
                        }
                    }
                }

                foundMatch = true;
            }
        }
    }
    return foundMatch;
}

Handling Input

Obviously, there has to be a way of moving pieces around on the board. I decided that the most intuitive way of doing this is to grab and drag pieces using the left mouse button. That is really all the input a simple board game needs, but I also wanted to allow the user to view the board from different angles. This means positioning the camera at different places. I decided that using the right mouse button and dragging should be used for this, and that scrolling the mouse wheel should zoom in and out. This means I have to handle MouseDown, MouseUp, MouseMove and MouseWheel in the GamePanel class:

C#
public void HandleMouseWheel(object sender, MouseEventArgs e)
{
    // If the user scrolls the mouse wheel we zoom out or in
    cameraDistanceFactor = Math.Max(0.0f, cameraDistanceFactor + 
            Math.Sign(e.Delta) / 5.0f);
    SetCameraPosition();
    Render();
}

private void GamePanel_MouseMove(object sender, MouseEventArgs e)
{
    // Dragging using the right mousebutton moves the camera 
    // along the X and Y axis.
    if (e.Button == MouseButtons.Right)
    {
        cameraAngle += (e.X - previousPoint.X) / 100.0f;
        cameraElevation = Math.Max(0, cameraElevation + 
                (e.Y - previousPoint.Y) / 10.0f);
        SetCameraPosition();
        previousPoint = e.Location;
    }

    Square square;
    if (e.Button == MouseButtons.Left)
    {
        if (ponderedMove != null)
        {
            if (board.GetMouseOverBlockModel
                (device, e.X, e.Y, out square, ponderedMove.Destinations))
            {
                // Set the dragged pieces location to the current square
                selectedPiecePosition.X = square.Column;
                selectedPiecePosition.Z = square.Row;
            }
        }
    }
    else
    {
        board.GetMouseOverBlockModel(device, e.X, e.Y, out square, 
            GamePanel.GetSquaresFromMoves(availableMoves));
    }

    // Render since we might have moved the camera
    Render();
}

private void GamePanel_MouseDown(object sender, MouseEventArgs e)
{
    // The previous point has to be set here or the distance dragged 
    // can be too big.
    previousPoint = e.Location;

    // If the mouse is over a block (see GetMouseOverBlockModel) 
    // for details on how determining that
    // and the left button is down, try to grab the piece 
    // (if there is one at the square and it has valid moves).
    if (e.Button == MouseButtons.Left)
    {
        ponderedMove = null;
        Square square;
        if (board.GetMouseOverBlockModel(device, e.X, e.Y, out square, null))
        {
            foreach (Move move in availableMoves)
            {
                // We have a move and it is started 
                // from the square we're over, start dragging a piece
                if (square.Equals(move.Origin))
                {
                    selectedPieceModel = board.PickUpPiece(square);
                    selectedPiecePosition = 
                    new Vector3(square.Column, 1.0f, square.Row);
                    ponderedMove = move;
                    break;
                }
            }
        }
    }
    Render();
}

private void GamePanel_MouseUp(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        Square square;
        if (board.GetMouseOverBlockModel(device, e.X, e.Y, out square, null))
        {
            // ponderedMove keeps track of the current potential move 
            // that will take place
            // if we drop the piece onto a valid square, if ponderedMove 
            // is not null that means
            // we're currently dragging a piece.
            if (ponderedMove != null)
            {
                foreach (Square allowedSquare in ponderedMove.Destinations)
                {
                    // Was it drop on a square that's a legal move?
                    if (square.Equals(allowedSquare))
                    {
                        // Move the piece to the target square
                        availableMoves = gameLogic.Move
                            (ponderedMove.Origin, allowedSquare);
                        break;
                    }
                }
            }
        }
        board.DropPiece();
        selectedPieceModel = null;
        Render();

        CheckForGameOver();
    }
}

Piece Control

The mouse methods check if they're called as a result of a left mouse button press. If they are, they use the VisualBoard.GetMouseOverBlockModel method to determine whether the event occurred when the cursor was over a specific square. This is then used to figure out if the user is allowed to pick up a piece from or drop a piece onto the current square. Also, VisualBoard.GetMouseOverBlockModel internally handles square highlighting automatically.

Camera Control

If the right mouse button is down when dragging the mouse, I can figure out the delta between two updates and use that information to update two members. A third member is updated when the mouse wheel is scrolled:

C#
private float cameraAngle = -((float)Math.PI / 2.0f);
private float cameraElevation = 7.0f;
private float cameraDistanceFactor = 1.5f;

Another method in GamePanel then uses that information to calculate a position for the camera. This position is constrained to a circle around the board (the radius is adjusted when zooming) and the camera is also constrained along the Y-axis to never go below zero:

C#
private void SetCameraPosition()
{
    // Calculate a camera position, this is a radius from the center 
    // of the board and then cameraElevation up.
    float cameraX = gameLogic.Columns / 
        2.0f + (cameraDistanceFactor * gameLogic.Columns * 
        (float)Math.Cos(cameraAngle));
    float cameraZ = gameLogic.Rows / 
        2.0f + (cameraDistanceFactor * gameLogic.Rows * 
        (float)Math.Sin(cameraAngle));

    camera.Position = new Vector3(
        cameraX, cameraElevation, cameraZ);
}

Creating Models

The class CheckersModelRepository is used to create all of the models used to render the Checkers game. It implements IBoardGameModelRepository so that the Framework has a generic way of accessing the models using the IBoardGameLogic data.

C#
class CheckersModelRepository
{
    ...
    public void Initialize(Microsoft.DirectX.Direct3D.Device device)
    {
        // Create a box to be used as a board square
        // The .Clone call is used to get a Mesh that has vertices that
        // contain both position, normal and color which I need to render 
        // them using flat shading.
        Mesh blockMesh = Mesh.Box(device, 1.0f, 0.5f, 1.0f).Clone
            (MeshFlags.Managed, VertexFormats.PositionNormal | 
            VertexFormats.Specular, device);

        // Create some red and black material and their 
        // highlighted counterparts.
        Material redMaterial = new Material();
        redMaterial.Ambient = Color.Red;
        redMaterial.Diffuse = Color.Red;

        Material highlightedRedMaterial = new Material();
        highlightedRedMaterial.Ambient = Color.LightSalmon;
        highlightedRedMaterial.Diffuse = Color.LightSalmon;

        Material squareBlackMaterial = new Material();
        Color squareBlack = Color.FromArgb(0xFF, 0x30, 0x30, 0x30);
        squareBlackMaterial.Ambient = squareBlack;
        squareBlackMaterial.Diffuse = squareBlack;

        Material blackMaterial = new Material();
        blackMaterial.Ambient = Color.Black;
        blackMaterial.Diffuse = Color.Black;

        Material highlightedBlackMaterial = new Material();
        highlightedBlackMaterial.Ambient = Color.DarkGray;
        highlightedBlackMaterial.Diffuse = Color.DarkGray;

        Material[] reds = new Material[] 
            { redMaterial, highlightedRedMaterial };
        Material[] blacks = new Material[] 
            { blackMaterial, highlightedBlackMaterial };

        blackSquare = new Model(blockMesh, new Material[] 
            { squareBlackMaterial, highlightedBlackMaterial });
        redSquare = new Model(blockMesh, reds);

        blackSquare.PositionOffset = new Vector3(0.0f, -0.25f, 0.0f);
        redSquare.PositionOffset = new Vector3(0.0f, -0.25f, 0.0f);

        // Create meshes for the pieces.
        Mesh pieceMesh = Mesh.Cylinder(device, 0.4f, 0.4f, 0.2f, 32, 1).Clone
            (MeshFlags.Managed, VertexFormats.PositionNormal | 
            VertexFormats.Specular, device);
        Mesh kingPieceMesh = 
            Mesh.Cylinder(device, 0.4f, 0.2f, 0.6f, 32, 1).Clone
            (MeshFlags.Managed, VertexFormats.PositionNormal | 
            VertexFormats.Specular, device);

        redPiece = new Model(pieceMesh, new Material[] 
            { redMaterial, redMaterial });
        blackPiece = new Model(pieceMesh, new Material[] 
            { blackMaterial, blackMaterial });
        redKingPiece = new Model(kingPieceMesh, new Material[] 
            { redMaterial, redMaterial });
        blackKingPiece = new Model(kingPieceMesh, new Material[] 
            { blackMaterial, blackMaterial });

        redPiece.PositionOffset = new Vector3(0.0f, 0.1f, 0.0f);
        redKingPiece.PositionOffset = new Vector3(0.0f, 0.3f, 0.0f);
        blackPiece.PositionOffset = new Vector3(0.0f, 0.1f, 0.0f);
        blackKingPiece.PositionOffset = new Vector3(0.0f, 0.3f, 0.0f);

        // The Mesh.Cylinder creates a cylinder that extends along the Z axis
        // but I want it to extend along the Y axis, this is easily fixed by
        // rotating it 90 degrees around the X axis.
        // First create the rotation...
        Quaternion rotation = Quaternion.RotationAxis
            (new Vector3(1.0f, 0.0f, 0.0f), (float)Math.PI / 2.0f);
        /// ... then apply it to all piece models
        redPiece.Orientation = rotation;
        blackPiece.Orientation = rotation;
        redKingPiece.Orientation = rotation;
        blackKingPiece.Orientation = rotation;
    }
}

Rotation and Translation

I will not explain in detail how 3D math is used to rotate and translate objects in 3D space, but I will explain what is done in the example implementation. Code statements like redPiece.PositionOffset = new Vector3(0.0f, 0.1f, 0.0f); are used to make sure that the "origin" of the model is offset by 0.1 along the Y-axis. This is done because Mesh::Cylinder creates a cylinder with the origin in the center of the cylinder and we need it to be at the edge of the cylinder for it to be placed correctly on the board. Also, we have to rotate in 90 degrees (PI / 2 radians) around the X-axis because it is created extending along the Z-axis and we want it to extend along the Y-axis. This is why this code is used:

C#
...

// The Mesh.Cylinder creates a cylinder that extends along the Z axis
// but I want it to extend along the Y axis, this is easily fixed by
// rotating it 90 degrees around the X axis.
// First create the rotation...
Quaternion rotation = Quaternion.RotationAxis(new Vector3(1.0f, 0.0f, 0.0f), 
        (float)Math.PI / 2.0f);
/// ... then apply it to all piece models
redPiece.Orientation = rotation;

...

Vertex Format

It is also important to have a Mesh that has a vertex format that suits our purposes. A vertex in a 3D model can contain different information depending on how it is going to be used. At the very least, Position data must be included. However, if the model is to have a color, Diffuse data must also be included. In the Framework, a directional light is used to shade the scene to look nicer. Because of this, Normal data must also be included.

The Mesh returned from the static methods on Mesh used to create different geometrical meshes (such as boxes and cylinders) does not return a Mesh with the vertex format we want. To fix this, we clone the mesh and pass the desired vertex format when cloning:

C#
// Create a box to be used as a board square
// The .Clone call is used to get a Mesh that has vertices that
// contain both position, normal and color which I need to render them 
// using flat shading.
Mesh blockMesh = Mesh.Box(device, 1.0f, 0.5f, 1.0f).Clone(MeshFlags.Managed,
    VertexFormats.PositionNormal | VertexFormats.Specular, device);

Code Clean-up

(This section added in version 2.)

When implementing a second game, Connect Four, I realized that the entire form setup could be reused and decided to provide an implementation in the API class library for this. This lessened the actual amount of code required when implementing a game. The form creation and startup code for the Checkers game is then reduced to this:

C#
is
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new GameForm(
        new CheckersLogic(),
        new CheckersModelRepository(),
        "Checkers",
        "Checkers, a most excellent game!"));
}

This creates a new GameForm object and passes IBoardGameLogic, IBoardGameModelRepository, the window title and the About text in the constructor. To be able to make the GameForm class able to display which players turn, it is that I had to add another method to IBoardGameLogic. I didn't want GameForm to have to poll the game logic for this information and decided to use a callback implemented with delegates. This required a new and additional method on the interface, as well as a delegate:

C#
public delegate void NextPlayerHandler(string playerIdentifier); 

public interface IBoardGameLogic
{
    ...
    
    void SetNextPlayerHandler(NextPlayerHandler nextPlayerHandler);
}

Now the form implementation can add one of its methods as NextPlayerHandler to the game logic. It is up to the game logic to indicate when the player changes. Super simple!

Connect Four Implementation

(This section added in version 2)

In order to show how easy it would be to implement another game, I decided to write a Connect Four game using this API. I chose Connect Four because it is fundamentally different from Checkers in some ways. I wanted to show that, regardless of these differences, it would be not only possible, but actually quite simple to implement.

The biggest difference is that in Connect Four, you do not start out with all the pieces on the board. Rather, you pick them from a pile and then place them on the board. By using a "logical" board that is larger that the board actually used, I created two areas from which an endless supply of pieces could be picked. By having IBoardGameModelRepository return null for the squares that weren't part of either the actual board or the "pile" areas, GamePanel could ignore rendering of these squares.

Example using Checkers logic

Shown above is the Connect Four implementation using very awesome-looking teapots as pieces. The actual game logic implementation for the Connect Four game is quite simple and took about the time it takes to take the train from London to Brighton and back again. :)

Final Result

So, how does the final implementation live up to the requirements I set out to fulfill? Sadly, I have to say that I failed to comply fully with requirement #3, that being that when implementing a new board game, the person implementing does not need to have any knowledge of 3D mathematics or Direct3D. The need for 3D knowledge can be seen in the CheckersModelRepository class where meshes are created, translated and rotated (rotated using scary quaternions, no less!). This is stuff that requires at least a beginner's knowledge of 3D mathematics.

This is also quite far from being a complete Framework, as it does not currently support a computer player. Furthermore, since I decided that I should not require any game loop, there is no smooth animation when moving pieces around. Other than that, I think it turned out quite well. It took me less than an hour to implement the Checkers game once the Framework was fully implemented and I think that indicates that it is easy to implement board games using this Framework.

I appreciate any comments, both on the code and on this article.

History

  • 2007/11/11: First version
  • 2007/11/25: Second version, added Connect Four and cleaned up the implementation

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)