370 lines
10 KiB
Text
370 lines
10 KiB
Text
Metadata-Version: 2.1
|
|
Name: board
|
|
Version: 1.0
|
|
Summary: Standard Board mechanism for Dojo tasks
|
|
Home-page: https://github.com/tjguk/dojo-board
|
|
Author: Tim Golden
|
|
Author-email: mail@timgolden.me.uk
|
|
Maintainer: Tim Golden
|
|
Maintainer-email: mail@timgolden.me.uk
|
|
License: unlicensed
|
|
Platform: UNKNOWN
|
|
Classifier: Environment :: Console
|
|
Classifier: Intended Audience :: Developers
|
|
Classifier: Operating System :: OS Independent
|
|
Classifier: Programming Language :: Python
|
|
Classifier: Programming Language :: Python :: 2
|
|
Classifier: Programming Language :: Python :: 2.7
|
|
Classifier: Programming Language :: Python :: 3
|
|
|
|
Board Game for Python Dojos
|
|
===========================
|
|
|
|
Introduction
|
|
------------
|
|
|
|
Often, when running a Python Dojo, we've ended up with a challenge
|
|
based around some kind of board or tile-based landscape. In these
|
|
situations it's not uncommon to spend a lot of the time building up
|
|
your basic board functionality in order to support the more interesting
|
|
gameplay algorithm.
|
|
|
|
This module implements a general-purpose board structure which
|
|
has the functionality needed for a range of purposes, and lends itself
|
|
to being subclassed for those particular needs.
|
|
|
|
Dependencies
|
|
------------
|
|
|
|
None - stdlib only
|
|
|
|
Tests
|
|
-----
|
|
|
|
Fairly decent coverage (not actually checked with coverage.py): test.py
|
|
|
|
Getting Started
|
|
---------------
|
|
|
|
Install with pip::
|
|
|
|
pip install board
|
|
|
|
Absolutely basic usage::
|
|
|
|
import board
|
|
#
|
|
# Produce a 3x3 board
|
|
#
|
|
b = board.Board((3, 3))
|
|
|
|
b[0, 0] = "X"
|
|
b[1, 0] = "O"
|
|
|
|
Usage
|
|
-----
|
|
|
|
Board is an n-dimensional board, any of which dimensions can be of
|
|
infinite size. (So if you have, say, 3 infinite dimensions, you have
|
|
the basis for a Minecraft layout). Dimensions are zero-based and
|
|
negative indexes operate as they usually do in Python: working from
|
|
the end of the dimension backwards.
|
|
|
|
Cells on the board are accessed by item access, eg board[1, 2] or
|
|
landscape[1, 1, 10].
|
|
|
|
A board can be copied, optionally along with its data by means of the
|
|
.copy method. Or a section of a board can be linked to the original
|
|
board by slicing the original board::
|
|
|
|
b1 = board.Board((9, 9))
|
|
b1[1, 1] = 1
|
|
b2 = b1.copy()
|
|
b3 = b1[:3, :3]
|
|
|
|
Note that the slice must include all the dimensions of the original
|
|
board, but any of those subdimensions can be of length 1::
|
|
|
|
b1 = board.Board((9, 9, 9))
|
|
b2 = b1[:3, :3, :1]
|
|
|
|
A sentinel value of Empty indicates a position which is not populated
|
|
because it has never had a value, or because its value has been deleted::
|
|
|
|
b1 = board.Board((3, 3))
|
|
assert b1[1, 1] is board.Empty
|
|
b1.populate("abcdefghi")
|
|
assert b1[1, 1] == "e"
|
|
del b1[1, 1]
|
|
assert b1[1, 1] is board.Empty
|
|
|
|
Iterating over the board yields its coordinates::
|
|
|
|
b1 = board.Board((2, 2))
|
|
for coord in b1:
|
|
print(coord)
|
|
#
|
|
# => (0, 0), (0, 1) etc.
|
|
#
|
|
|
|
Iteration over a board with one or more infinite dimensions will work
|
|
by iterating in chunks::
|
|
|
|
b1 = board.Board((3, 3, board.Infinity))
|
|
for coord in b1:
|
|
print(b1)
|
|
|
|
To see coordinates with their data items, use iterdata::
|
|
|
|
b1 = board.Board((2, 2))
|
|
b1.populate("abcd")
|
|
for coord, data in b1.iterdata():
|
|
print(coord, "=>", data)
|
|
|
|
To read, write and empty the data at a board position, use indexing::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate("abcdef")
|
|
print(b1[0, 0]) # "a"
|
|
|
|
b1[0, 0] = "*"
|
|
print(b1[0, 0]) # "*"
|
|
|
|
b1[-1, -1] = "*"
|
|
print(b1[2, 2]) # "*"
|
|
|
|
del b1[0, 0]
|
|
print(b1[0, 0]) # <Empty>
|
|
|
|
To test whether a coordinate is contained with the local coordinate space, use in::
|
|
|
|
b1 = board.Board((3, 3))
|
|
(1, 1) in b1 # True
|
|
(4, 4) in b1 # False
|
|
(1, 1, 1) in b1 # InvalidDimensionsError
|
|
|
|
One board is equal to another if it has the same dimensionality and
|
|
each data item is equal::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate("abcdef")
|
|
b2 = b1.copy()
|
|
b1 == b2 # True
|
|
b2[0, 0] = "*"
|
|
b1 == b2 # False
|
|
|
|
b2 = board.Board((2, 2))
|
|
b2.populate("abcdef")
|
|
b1 == b2 # False
|
|
|
|
To populate the board from an arbitrary iterator, use .populate::
|
|
|
|
def random_letters():
|
|
import random, string
|
|
while True:
|
|
yield random.choice(string.ascii_uppercase)
|
|
|
|
b1 = board.Board((4, 4))
|
|
b1.populate(random_letters())
|
|
|
|
To clear the board, use .clear::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate(range(10))
|
|
b1.clear()
|
|
list(b1.iterdata()) # []
|
|
|
|
A board is True if it has any data, False if it has none::
|
|
|
|
b1 = board.Board((2, 2))
|
|
b1.populate("abcd")
|
|
bool(b1) # True
|
|
b1.clear()
|
|
bool(b1) # False
|
|
|
|
The length of the board is the product of its dimension lengths. If any
|
|
dimension is infinite, the board length is infinite. NB to find the
|
|
amount of data on the board, use lendata::
|
|
|
|
b1 = board.Board((4, 4))
|
|
len(b1) # 16
|
|
b1.populate("abcd")
|
|
len(b1) # 16
|
|
b1.lendata() # 4
|
|
b2 = board.Board((2, board.Infinity))
|
|
len(b2) # Infinity
|
|
|
|
To determine the bounding box of the board which contains data, use .occupied::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate("abcd")
|
|
list(c for (c, d) in b1.iterdata()) # [(0, 0), (0, 1), (0, 2), (1, 0)]
|
|
b1.occupied() # ((0, 0), (1, 2))
|
|
|
|
For the common case of slicing a board around its occupied space,
|
|
use .occupied_board::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate("abcd")
|
|
b1.draw()
|
|
b2 = b1.occupied_board()
|
|
b2.draw()
|
|
|
|
To test whether a position is on any edge of the board, use .is_edge::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.is_edge((0, 0)) # True
|
|
b1.is_edge((1, 1)) # False
|
|
b1.is_edge((2, 0)) # True
|
|
|
|
To find the immediate on-board neighbours to a position along all dimensions::
|
|
|
|
b1 = board.Board((3, 3, 3))
|
|
list(b1.neighbours((0, 0, 0)))
|
|
# [(0, 1, 1), (1, 1, 0), ..., (1, 0, 1), (0, 1, 0)]
|
|
|
|
To iterate over all the coords in the rectangular space between
|
|
two corners, use .itercoords::
|
|
|
|
b1 = board.Board((3, 3))
|
|
list(b1.itercoords((0, 0), (1, 1))) # [(0, 0), (0, 1), (1, 0), (1, 1)]
|
|
|
|
To iterate over all the on-board positions from one point in a
|
|
particular direction, use .iterline::
|
|
|
|
b1 = board.Board((4, 4))
|
|
start_from = 1, 1
|
|
direction = 1, 1
|
|
list(b1.iterline(start_from, direction)) # [(1, 1), (2, 2), (3, 3)]
|
|
direction = 0, 2
|
|
list(b1.iterline(start_from, direction)) # [(1, 1), (1, 3)]
|
|
|
|
or .iterlinedata to generate the data at each point::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate("ABCDEFGHJ")
|
|
start_from = 1, 1
|
|
direction = 1, 0
|
|
list(b1.iterlinedata(start_from, direction)) # ['A', 'D', 'G']
|
|
|
|
Both iterline and iterdata can take a maximum number of steps, eg for
|
|
games like Connect 4 or Battleships::
|
|
|
|
b1 = board.Board((8, 8))
|
|
#
|
|
# Draw a Battleship
|
|
#
|
|
b1.populate("BBBB", b1.iterline((2, 2), (1, 0)))
|
|
|
|
As a convenience for games which need to look for a run of so many
|
|
things, the .run_of_n method combines iterline with data to yield
|
|
every possible line on the board which is of a certain length along
|
|
with its data::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1[0, 0] = 'X'
|
|
b1[1, 1] = 'O'
|
|
b1[0, 1] = 'X'
|
|
for line, data in b1.runs_of_n(3):
|
|
if all(d == "O" for d in data):
|
|
print("O wins")
|
|
break
|
|
elif all(d == "X" for d in data):
|
|
print("X wins")
|
|
break
|
|
|
|
To iterate over the corners of the board, use .corners::
|
|
|
|
b1 = board.Board((3, 3))
|
|
corners() # [(0, 0), (0, 2), (2, 0), (2, 2)]
|
|
|
|
Properties
|
|
----------
|
|
|
|
To determine whether a board is offset from another (ie the result of a slice)::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.is_offset # False
|
|
b2 = b1[:1, :1]
|
|
b2.is_offset # True
|
|
|
|
To determine whether a board has any infinite or finite dimensions::
|
|
|
|
b1 = board.Board((3, board.Infinity))
|
|
b1.has_finite_dimensions # True
|
|
b1.has_infinite_dimensions # True
|
|
b2 = board.Board((3, 3))
|
|
b1.has_infinite_dimensions # False
|
|
b3 = board.Board((board.Infinity, board.Infinity))
|
|
b3.has_finite_dimensions # False
|
|
|
|
Display the Board
|
|
-----------------
|
|
|
|
To get a crude view of the contents of the board, use .dump::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate("abcdef")
|
|
b1.dump()
|
|
|
|
To get a grid view of a 2-dimensional board, use .draw::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1.populate("OX XXOO ")
|
|
b1.draw()
|
|
|
|
If you don't want the borders drawn, eg because you're using the board
|
|
to render ASCII art, pass use_borders=False::
|
|
|
|
b1 = board.Board((8, 8))
|
|
for coord in b1.iterline((0, 0), (1, 1)):
|
|
b1[coord] = "*"
|
|
for coord in b1.iterline((7, 0), (-1, 1)):
|
|
b1[coord] = "*"
|
|
b1.draw(use_borders=False)
|
|
|
|
To render to an image using Pillow (which isn't a hard dependency) use paint.
|
|
The default renderer treats the data items as text and renders then, scaled
|
|
to fit, into each cell. This works, obviously, for things like Noughts & Crosses
|
|
assuming that you store something like "O" and "X". But it also works for
|
|
word searches and even simple battleships where the data items are objects
|
|
whose __str__ returns blank (for undiscovered), "+" for a single hit, and "*"
|
|
for a destroyed vessel::
|
|
|
|
b1 = board.Board((3, 3))
|
|
b1[0, 0] = "X"
|
|
b1[1, 1] = "O"
|
|
b1[0, 2] = "X"
|
|
b1.paint("board.png")
|
|
# ... and now look at board.png
|
|
|
|
The text painting is achieved internally by means of a callback called
|
|
text_sprite. An alternative ready-cooked callback for paint() is
|
|
imagefile_sprite. This looks for a .png file in the current directory
|
|
(or another; you can specify).
|
|
|
|
Local and Global coordinates
|
|
----------------------------
|
|
|
|
Since one board can represent a slice of another, there are two levels
|
|
of coordinates: local and global. Coordinates passed to or returned from
|
|
any of the public API methods are always local for that board. They
|
|
represent the natural coordinate space for the board. Internally, the
|
|
module will use global coordinates, translating as necessary.
|
|
|
|
Say you're managing a viewport of a tile-based dungeon game where the
|
|
master dungeon board is 100 x 100 but the visible board is 10 x 10.
|
|
Your viewport board is currently representing the slice of the master
|
|
board from (5, 5) to (14, 14). Changing the item at position (2, 2) on
|
|
the viewport board will change the item at position (7, 7) on the master
|
|
board (and vice versa).
|
|
|
|
As a user of the API you don't need to know this, except to understand
|
|
that a board slice is essentially a view on its parent. If you wish
|
|
to subclass or otherwise extend the board, you'll need to note where
|
|
coordinate translations are necessary.
|
|
|
|
|
|
|
|
|
|
|