Omok (오목)
Omok is an open-source Python library for developing AI for Omok (Gomoku) and Connect6.
Install
$ pip install omok
$ pip install "omok[fast]" # optional: numba-compiled Renju rule, ~30x faster
With the fast extra, Renju forbidden move detection is compiled with numba
(the first run takes a few seconds to compile, later runs use the on-disk cache).
Without it, a pure Python implementation is used. Set OMOK_DISABLE_NUMBA=1 to force the pure Python version.
Development
This project uses uv.
$ uv sync # create a virtual environment and install dependencies
$ uv run pytest # run tests
$ uv run python -m omok
$ uv build # build distribution packages
Usage
Play
$ python -m omok
A web server starts at http://127.0.0.1:8000 and a browser opens automatically.
Click the board to place a stone. Keyboard shortcuts: A (AI move), B (undo), Space (reset), S (save game record).
Turn on Show AI probabilities (shortcut P) to show the AI's move probabilities (%) for the current turn as a heatmap on the board.
$ python -m omok --port 8080 --no-browser # set the port, don't open a browser
$ python -m omok --no-agent # run without the AI
$ python -m omok --game connect6 # Connect6 (no AI)
Rule
The default rule is Renju. Black may not play a double-three (3-3), a double-four (4-4), or an overline (six or more in a row), and wins only with exactly five. If a move makes a five and a forbidden shape at the same time, the five takes priority. White has no restrictions, and an overline also counts as a win for White. In the web UI, forbidden points are marked with a red × on Black's turn.
$ python -m omok --rule freestyle # freestyle, no forbidden moves
env = omok.Omok(rule='freestyle') # rule='renju' by default
env.get_forbidden() # Black's current forbidden points (Renju only, on Black's turn; otherwise [])
Environment
import omok
env = omok.Omok()
for move in [112, 111, 96, 97, 128, 113, 80, 127, 144]:
env(move)
print(env)
"""
Result
+-------------------------------+
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - O - - - - - - - - - |
| - - - - - - O X - - - - - - - |
| - - - - - - X O X - - - - - - |
| - - - - - - - X O - - - - - - |
| - - - - - - - - - O - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
| - - - - - - - - - - - - - - - |
+-------------------------------+
| Player 2 Winner 1 Moves 9 |
+-------------------------------+
"""
Agent
import omok
agent = omok.OmokAgent(model_index=1)
env = omok.Omok()
while True:
state = env.get_state()
player = env.get_player()
action = agent(state, player)
env(action)
print(env)
if env.is_done():
break
Reinforcement Learning
import numpy as np
import omok
env = omok.Omok()
while not env.is_done():
obs = env.get_observation() # (5, 15, 15) float32, see below
mask = env.get_legal_mask() # (225,) bool: legal moves (excludes Renju forbidden points)
action = np.random.choice(np.flatnonzero(mask))
env(action) # 0: continue, 1: game over, -1: illegal move
print(env.get_winner()) # 0: draw, 1: black wins, 2: white wins
Gymnasium-style step()
env = omok.Omok()
obs, info = env.reset()
terminated = False
while not terminated:
action = np.random.choice(np.flatnonzero(info['action_mask']))
obs, reward, terminated, truncated, info = env.step(action)
print(reward, info['winner']) # reward is for the player who just moved (1 for a win, else 0)
obsandinfo['action_mask']are for the player to move next;rewardis for the player who just moved.step()raisesValueErroron an illegal move or when the game is already over.- The Renju forbidden points are cached until the board changes, so repeated
get_legal_mask()calls are cheap. - Renju env steps are about 2x slower than freestyle with numba, and about 50-70x slower without it.
get_state()andget_move_history()return copies, so they are safe to store as-is.- A position out of range (outside
0 <= pos < 225) raisesValueError. - Undo moves with
move_back().
Observation planes of get_observation(), all from the current player's view:
| Plane | Omok (5, 15, 15) |
Connect6 (5, 19, 19) |
|---|---|---|
| 0 | own stones | own stones |
| 1 | opponent stones | opponent stones |
| 2 | 1 if black to play | 1 if black to play |
| 3 | last stone placed | last stone placed |
| 4 | forbidden points (Renju, black to play) | 1 if the next stone is the last one of the turn |
Search (e.g. MCTS)
env = omok.Omok.from_moves([112, 111, 96]) # replay moves; kwargs go to the constructor (rule=...)
child = env.clone() # independent copy, ~6x faster than copy.deepcopy
child.move(97)
Symmetry augmentation
from omok import transforms
obs = env.get_observation()
policy = np.zeros(225, np.float32) # e.g. MCTS visit distribution
for obs_t, policy_t in transforms.symmetries(obs, policy): # 8 rotations/reflections
...
transforms.transform_board, transform_action and transform_policy apply a single symmetry code (0-7)
and its inverse, and work for any board size.
Connect6
import omok
env = omok.Connect6() # 19x19, six or more in a row wins
for move in [180, 179, 161, 160, 200, 181, 199, 140, 220, 198, 162, 120]:
env(move) # black opens with 1 stone, then each side places 2 per turn
print(env.get_winner()) # 1: black completes six with the last move
Connect6 provides the same interface as Omok (step, get_legal_mask, get_observation, from_moves, clone, move_back, etc.).
License
MIT
Metadata
Release files for omok 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| omok-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / omok-0.1.0-py3-none-any.whl
| Download URL | omok-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f86568f1e227d56e22b1b6caab8ff7bc88b642f6c825f2de8a7ddebc7ed95d66
|
|
BLAKE2b-256 checksum How to use checksums |
4f0ca3fe9aff6936637186928d3c60b625fc9b8401293659dc9331fb589ee448
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|