Skip to content

Latest commit

 

History

History
637 lines (493 loc) · 24.3 KB

README.en.md

File metadata and controls

637 lines (493 loc) · 24.3 KB

reversi

[ English | 日本語]

reversi is a library for Reversi (Othello) for Python.
You can easily program reversi AI and create applications.
MIT License unittest

目次

Overview

reversi is a Reversi library made in Python1 that can be used with Python.
After installing reversi, you can easily experiment with Reversi AI programming.

Other uses include

  • Create an application and play against your own AI.
  • Use simulators to test the strength of AIs by playing them against each other.

A Windows version of the application created using this library is also available.
This one allows you to play a game of reversi for free immediately after downloading, with no installation required.

System Requirements

  • Windows, Ubuntu, MacOS
  • Display size 1366x768
  • Processor 1.6GHz
  • Memory 4.00GB
  • Python 3.7.6(3.11 for MacOS)
    • cython 0.29.15
    • pyinstaller 3.6
  • Microsoft Visual C++ 2019(When developing on Windows)

How to Install

  1. install Python 3.7.6 or higher
  2. install reversi(run the following)
$ pip install git+https://github.com/y-tetsu/reversi

How to Uninstall

uninstall reversi(run the following)

$ pip uninstall reversi

Examples

Run the following in any folder to copy the examples.

$ install_reversi_examples

The examples to be copied are below.

You can run examples below.

$ cd reversi_examples
$ python 01_tkinter_app.py
$ python 02_console_app.py
$ 03_create_exe.bat
$ python 04_reversi_simulator.py
$ python 05_easy_strategy.py
$ python 06_table_strategy.py
$ python 07_minmax_strategy.py
$ python 08_alphabeta_strategy.py
$ python 09_genetic_algorithm.py
$ python 10_variant_board_solver.py

How to use the library

Basics

This section explains the basic usage of this library based on coding examples.

Launching an application

First, we will show how to launch a reversi GUI application.

Execute the following code.

from reversi import Reversi

Reversi().start()

The application will start and you can play with two players as it is.
(In this case, the only player that can be selected is the user operation)

Adding an AI to your application

Next, we show how to add an AI to your application.

As an example, we add the following AI, which is pre-loaded in the library, to your application.

  • AI that plays random moves : Random.
  • AI that plays moves to get as many stones as possible :Greedy.
from reversi import Reversi
from reversi.strategies import Random, Greedy

Reversi(
    {
        'RANDOM': Random(),
        'GREEDY': Greedy(),
    }
).start()

After the above is done, "RANDOM" and "GREEDY" can be selected as the player in addition to the user operation.

All built-in AIs can be imported from reversi.strategies.
For more information on other available AIs, please refer to AI class.
The player information to be added (hereinafter referred to as "player information") should follow the format below.
Player names can be set arbitrarily.

{
    'PlayerName1': AI-Object1,
    'PlayerName2': AI-Object2,
    'PlayerName3': AI-Object3,
}

Programming an AI

The following shows how to use this library to create your own AI and add it to your application.

How to create an AI class

Coding as follows will complete the AI class.

from reversi.strategies import AbstractStrategy

class OriginalAI(AbstractStrategy):
    def next_move(self, color, board):
        #
        # Please code the logic to determine the next move (X, Y).
        #

        return (X, Y)

The next_move method implements logic to return where to place a stone (the next move) at a particular move number and board level.
See below for the arguments of the next_move method.

Arguments Description
color variable black or white string of type str is passed to determine whether the number is black or white, respectively.
board object object containing information about the reversi board. The object contains parameters and methods necessary for the game to progress, such as the placement of black and white stones, and the acquisition of the positions where stones can be placed.

Note that the (X, Y) coordinates of the return value are the values when the upper left corner of the board is set to (0, 0).
The following figure shows the coordinates of each square when the board size is 8.

coordinate

As for the board object, for simplicity, we will focus on two of them: the get_legal_moves method to get the position where a stone can be placed, and the size parameter to get the size of the board. For a more detailed explanation, please refer to How to use the board object.

How to get the positions where stones can be placed

You can get the positions (coordinates) where stones can be placed on a board by using the get_legal_moves method of the board object.
When calling get_legal_moves, give either the black or white move number (color variable) as an argument.

legal_moves = board.get_legal_moves(color)

The return value of get_legal_moves is "a list of coordinates where stones can be placed".

The result of the black move with the initial state (board size 8) is as follows.

[(3, 2), (2, 3), (5, 4), (4, 5)]
Board Size

This application is designed to allow an even number of board sizes from 4 to 26 to be selected. If necessary, please consider the size of the board so that it will work in either case.

The size of the board can be obtained by the following description.

size = board.size
Implementing the "Always take a corner when you can" AI

The following is an example of creating an AI named Corner that always takes 4 corners when it can, and makes random moves when it cannot(The player name is "CORNER").

import random

from reversi import Reversi
from reversi.strategies import AbstractStrategy

class Corner(AbstractStrategy):
    def next_move(self, color, board):
        size = board.size
        legal_moves = board.get_legal_moves(color)
        for corner in [(0, 0), (0, size-1), (size-1, 0), (size-1, size-1)]:
            if corner in legal_moves:
                return corner

        return random.choice(legal_moves)

Reversi({'CORNER': Corner()}).start()

After the above is done, "CORNER" can be selected as a player to play against.
If you actually play against a player, you will see that he will always take the corner when he can!

Simulate a match between AIs

The simulator in this library can be used to play AIs against each other multiple times to produce a winning percentage.
Use it to measure the strength of your own AI.
If the AI's moves are fixed for a particular board, you can reduce the bias of the results by setting the random_opening parameter described separately below.

As an example of running the simulator The following is an example of running the simulator, showing the "RANDOM," "GREEDY," and "CORNER" games that have appeared so far in a round-robin competition, up to the display of the results.

Running the Simulator

Execute the following to start the simulation.

from reversi import Simulator

if __name__ == '__main__':
    simulator = Simulator(
        {
            'RANDOM': Random(),
            'GREEDY': Greedy(),
            'CORNER': Corner(),
        },
        './simulator_setting.json',
    )
    simulator.start()

The simulator must be executed in the main module (__main__).
The simulator argument should be "player information" and "simulator configuration file".

Simulator Configuration File

An example of creating a simulator configuration file (JSON format) is shown below. Specify the name of this file (in the above example, . /simulator_setting.json in the above example, but it is optional.

{
    "board_size": 8,
    "matches": 100,
    "processes": 1,
    "parallel": "player",
    "random_opening": 0,
    "player_names": [
        "RANDOM",
        "GREEDY",
        "CORNER"
    ]
}
parameter name description
board_size Specify the size of the board.
matches Specify the number of games played between AIs. 100 means 100 games for each combination of AIs, one for the first move and one for the second move.
processes Specify the number of parallel runs. The larger the setting, the faster the simulation results may be obtained, depending on the number of cores in your PC.
parallel Specify the unit of parallel execution. If you specify "player" (default), parallel processing is performed for each combination of AI matches. If "game" is specified, the number of matches in "matches" is divided by the number of processes and parallel processing is performed. If the number of AI matchups to be simulated is smaller than the number of cores in your PC, you may get faster results by specifying "game".
random_opening From the start of the game until the specified number of moves, the AI will play random moves against each other. When the number of moves exceeds the specified number, the AI will play its original move. By randomizing the starting situation of the game, it reduces the bias of the result and makes it easier to measure the strength of the AI. If you don't need it, please specify 0.
player_names List the names of the AIs you want to play against. If specified, select one of the names included in the first argument "player information". If omitted, it is treated as the same as the first argument "player information". All listed AIs compete against each other in a round-robin game.
Execution Result

The simulation results can be seen in the printout below after the simulation is run (after the start method is executed).

print(simulator)
Running example

Run the simulator against players "RANDOM" and "GREEDY" using the library's built-in AI, and "CORNER" using our own AI. and "CORNER" using our own AI, respectively, and outputs the results. The following is an example of the code to run the simulator and output the results.

import random

from reversi import Simulator
from reversi.strategies import AbstractStrategy, Random, Greedy

class Corner(AbstractStrategy):
    def next_move(self, color, board):
        size = board.size
        legal_moves = board.get_legal_moves(color)
        for corner in [(0, 0), (0, size-1), (size-1, 0), (size-1, size-1)]:
            if corner in legal_moves:
                return corner

        return random.choice(legal_moves)

if __name__ == '__main__':
    simulator = Simulator(
        {
            'RANDOM': Random(),
            'GREEDY': Greedy(),
            'CORNER': Corner(),
        },
        './simulator_setting.json',
    )
    simulator.start()

    print(simulator)

The following results were obtained after 100 rounds of "RANDOM," "GREEDY," and "CORNER" games, played first and second hand, respectively.

Size : 8
                          | RANDOM                    GREEDY                    CORNER
---------------------------------------------------------------------------------------------------------
RANDOM                    | ------                     32.5%                     21.5%
GREEDY                    |  66.0%                    ------                     29.5%
CORNER                    |  76.0%                     68.5%                    ------
---------------------------------------------------------------------------------------------------------

                          | Total  | Win   Lose  Draw  Match
------------------------------------------------------------
RANDOM                    |  27.0% |   108   284     8   400
GREEDY                    |  47.8% |   191   202     7   400
CORNER                    |  72.2% |   289   102     9   400
------------------------------------------------------------

The results show that it seems to be more advantageous to take more each time than to hit randomly, and even more so to always take a corner.

Objects

This section describes the various objects provided in this library.

How to use board object

This section describes how to use the board object, which manages the reversi board.

Creating a board object

A board object can be created by importing the Board class from reversi. Import the Board class from reversi to create a board object.

When instantiating the Board class, you can specify the size of the board by entering a numerical value as an argument. The size should be an even number from 4 to 26. If omitted, the size is 8. You can also check the size of the board with the size property.

A coding example is shown below.

from reversi import Board

board = Board()
print(board.size)

board = Board(10)
print(board.size)

The results of the above execution are as follows.

8
10
Standard output of a board object

When you print a board object, the state of the board is printed as standard output.

from reversi import Board

board = Board()
print(board)

board = Board(4)
print(board)

The results of the above execution are as follows.
board_print

Methods of the board object

Here are the available methods of the board object.

get_legal_moves

Returns the possible move positions for the black or white board. The possible moves are a "list of tuples of XY coordinates". The argument must be a black or white string (hereafter called color).

from reversi import Board

board = Board()
legal_moves = board.get_legal_moves('black')

print(legal_moves)

The results of the above execution are as follows.

[(3, 2), (2, 3), (5, 4), (4, 5)]

In this case, the position of the yellow square in the figure below is returned as the possible starting position.
legal_moves

get_flippable_discs

Returns the stones that can be flipped if the move is made at the specified position. The flippable stones are "a list of tuples of XY coordinates". The first argument is the color, the second is the X coordinate at which to place the stone, and the third is the Y coordinate.

from reversi import Board

board = Board()
flippable_discs = board.get_flippable_discs('black', 5, 4)

print(flippable_discs)

The results of the above execution are as follows.

[(4, 4)]

In this case, the position of the yellow square in the diagram below is returned as the position of the stone that can be turned over.
flippable_discs

get_board_info

Returns a "two-dimensional list" of stones on the board. 1" means black, "-1" means white, and "0" means empty. There is no argument.

from pprint import pprint
from reversi import Board

board = Board()
board_info = board.get_board_info()

print(board)
pprint(board_info)

The results of the above execution are as follows.
get_board_info

get_board_line_info

Returns information about the board as a one-line string.

from pprint import pprint
from reversi import Board

board = Board()

print(board.get_board_line_info('black'))

The results of the above execution are as follows.

---------------------------O*------*O---------------------------*

The format is: board square information + player information.

The argument must be a string ('black' or 'white') indicating the player.

The default character assignments are as follows.

  • "*" : black player
  • "O" : white player
  • "-" : empty square

You can change it to a character of your choice by specifying an optional argument.

print(board.get_board_line_info(player='black', black='0', white='1', empty='.'))

The above execution produces the following output.

...........................10......01...........................0
  • player : Please specify 'black' or 'white'.
  • black : Specify the character to be assigned to black
  • white : Specify the character to be assigned to the white square.
  • empty : Specify the character to be assigned to the empty square
put_disc

Places a stone at the specified position and turns over the stone to be taken. Specify color as the first argument, X coordinate to place the stone as the second argument, and Y coordinate as the third argument.

from reversi import Board

board = Board()
print(board)

board.put_disc('black', 5, 4)
print(board)

The above execution produces the following output.
put_disc

undo

Undo a stone placed by the put_disc method. There is no argument. You can undo as many times as you call the put_disc method. Do not call this method more times than you called the put_disc method.

from reversi import Board

board = Board()
board.put_disc('black', 5, 4)
print(board)

board.undo()
print(board)

The above execution produces the following output.
undo

How to use color object

So far, we have shown how to use the string black' or white' to determine the black or white move number, but it is also possible to specify the black or white move number using a color object.

You can import a color object, C, as shown below, and specify the black and white numbers with the black and white properties, respectively.

from reversi import Board
from reversi import C as c

board = Board()
board.put_disc(c.black, 5, 4)
print(board)

board.put_disc(c.white, 5, 5)
print(board)

The above execution produces the following output.
color

How to use move object

The move object allows you to specify the coordinates of a move not only in XY coordinate format, but also in str format, such as 'a1', 'c3', etc. The str format allows both upper and lower case alphabets.

Import the Move class and use it as follows.

from reversi import Board
from reversi import C as c
from reversi import Move as m

board = Board()
board.put_disc(c.black, *m('f5'))
print(board)

board.put_disc(c.white, *m('f6'))
print(board)

The above execution produces the following output.
color

Also, a A move object can also be generated in XY coordinate format and converted to str format using the str function. If you print a move object, it will be in str format as well, and if you specify upper as the case option, it will be in upper case.

from reversi import Board
from reversi import Move as m

move = str(m(5, 4))
print(move)
print(m(5, 5, case='upper'))

The above execution produces the following output.

f5
F6

Footnotes

[1]: Cython is used in some parts.


License

The source code of this repository is MIT License. Please feel free to use it for both commercial and non-commercial purposes.