board
Advanced tools
+109
-24
@@ -1,10 +0,11 @@ | ||
| Metadata-Version: 1.1 | ||
| Metadata-Version: 1.2 | ||
| Name: board | ||
| Version: 0.0.0.post0 | ||
| Version: 1.0 | ||
| Summary: Standard Board mechanism for Dojo tasks | ||
| Home-page: https://github.com/graingert/dojo-board | ||
| Author: Thomas Grainger | ||
| Author-email: board@graingert.co.uk | ||
| 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 | ||
| Description-Content-Type: UNKNOWN | ||
| Description: Board Game for Python Dojos | ||
@@ -59,3 +60,5 @@ =========================== | ||
| infinite size. (So if you have, say, 3 infinite dimensions, you have | ||
| the basis for a Minecraft layout). Dimensions are zero-based. | ||
| 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. | ||
@@ -113,3 +116,2 @@ Cells on the board are accessed by item access, eg board[1, 2] or | ||
| To read, write and empty the data at a board position, use indexing:: | ||
@@ -124,2 +126,5 @@ | ||
| b1[-1, -1] = "*" | ||
| print(b1[2, 2]) # "*" | ||
| del b1[0, 0] | ||
@@ -149,15 +154,2 @@ print(b1[0, 0]) # <Empty> | ||
| 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() | ||
| To populate the board from an arbitrary iterator, use .populate:: | ||
@@ -189,3 +181,3 @@ | ||
| The length of the board is the product of its dimension lengths. If any | ||
| dimension is infinite, the board length is infinte. NB to find the | ||
| dimension is infinite, the board length is infinite. NB to find the | ||
| amount of data on the board, use lendata:: | ||
@@ -208,2 +200,11 @@ | ||
| 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:: | ||
@@ -222,3 +223,3 @@ | ||
| EXPERIMENTAL: To iterate over all the coords in the rectangular space between | ||
| To iterate over all the coords in the rectangular space between | ||
| two corners, use .itercoords:: | ||
@@ -229,3 +230,3 @@ | ||
| EXPERIMENTAL: To iterate over all the on-board positions from one point in a | ||
| To iterate over all the on-board positions from one point in a | ||
| particular direction, use .iterline:: | ||
@@ -240,2 +241,41 @@ | ||
| 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 | ||
@@ -261,2 +301,47 @@ ---------- | ||
| 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 | ||
@@ -263,0 +348,0 @@ ---------------------------- |
+303
-49
@@ -1,6 +0,8 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # -*- coding: utf-8-*- # Encoding cookie added by Mu Editor | ||
| """Board -- an n-dimensional board with support for iteration, containership and slicing | ||
| Boards can have any number of dimensions, any of which can be infinite. Boards | ||
| can be sliced [:1, :2], returning a linked-copy, or copied (.copy), returning a snapshot copy. | ||
| can be sliced [:1, :2], returning a linked-copy, or copied (.copy), returning a | ||
| snapshot copy. | ||
@@ -10,5 +12,6 @@ Boards can be iterated over for coordinates or data (.iterdata). There are also | ||
| the bounding box of occupied data (.occupied), all the coordinates in a space | ||
| in n-dimensions (.itercoords) and other. | ||
| in n-dimensions (.itercoords) and others. | ||
| """ | ||
| # testing | ||
| # | ||
@@ -30,3 +33,9 @@ # The semantics of 3.x range are broadly equivalent | ||
| import itertools | ||
| import io | ||
| try: | ||
| from PIL import Image, ImageDraw, ImageFont | ||
| except ImportError: | ||
| Image = None | ||
| class _Infinity(int): | ||
@@ -61,2 +70,3 @@ | ||
| return False | ||
| __nonzero__ = __bool__ | ||
@@ -155,2 +165,57 @@ Empty = _Empty() | ||
| def _centred_coord(outer_size, inner_size): | ||
| """Given an outer and an inner size, calculate the top-left coordinates | ||
| which the inner image should position at to be centred within the outer | ||
| image | ||
| """ | ||
| outer_w, outer_h = outer_size | ||
| inner_w, inner_h = inner_size | ||
| return round((outer_w - inner_w) / 2), round((outer_h - inner_h) / 2) | ||
| def text_sprite(font_name="arial", colour="#0000ff"): | ||
| """Text sprite generator callback from Board.paint | ||
| Convert the object to text of approximately the right size for | ||
| the cell being painted. Typically this will be used for one or | ||
| two letter objects, but it will work for any object which can | ||
| meaningfully be converted to text | ||
| """ | ||
| def _text_sprite(obj, size): | ||
| # | ||
| # Very roughly, one point is three quarters of | ||
| # a pixel. We pick a point size which will fill | ||
| # the smaller edge of the cell (if it's not square) | ||
| # | ||
| point_size = round(min(size) * 0.75) | ||
| # | ||
| # Create a new transparent image to hold the | ||
| # text. Draw the text into it in blue, centred, | ||
| # using the font requested, and return the resulting image | ||
| # | ||
| image = Image.new("RGBA", size, (255, 255, 255, 0)) | ||
| draw = ImageDraw.Draw(image) | ||
| font = ImageFont.truetype("%s.ttf" % font_name, point_size) | ||
| text = str(obj) | ||
| draw.text(_centred_coord(size, font.getsize(text)), text, font=font, fill=colour) | ||
| return image | ||
| return _text_sprite | ||
| def imagefile_sprite(directory=".", extension=".png"): | ||
| """Image sprite generator callback for Board.paint | ||
| Given the text form of an object, look for an image file in the | ||
| stated directory [default: current] and return it, scaled to size. | ||
| """ | ||
| def _imagefile_sprite(obj, size): | ||
| image = Image.open(os.path.join(directory, "%s%s" % (obj, extension))) | ||
| image.thumbnail(size) | ||
| return image | ||
| return _imagefile_sprite | ||
| class Board(object): | ||
@@ -166,6 +231,6 @@ """Board - represent a board of n dimensions, each possibly infinite. | ||
| b[2, 2] = "*" | ||
| print(b[2, 2]) | ||
| b.draw() | ||
| """ | ||
| class BoardError(BaseException): pass | ||
| class BoardError(Exception): pass | ||
| class InvalidDimensionsError(BoardError): pass | ||
@@ -179,2 +244,6 @@ class OutOfBoundsError(BoardError): pass | ||
| raise self.InvalidDimensionsError("The board must have at least one dimension") | ||
| try: | ||
| iter(dimension_sizes) | ||
| except TypeError: | ||
| raise self.InvalidDimensionsError("Dimensions must be iterable (eg a tuple), not {}".format(type(dimension_sizes).__name__)) | ||
| if any(d <= 0 for d in dimension_sizes): | ||
@@ -192,2 +261,3 @@ raise self.InvalidDimensionsError("Each dimension must be >= 1") | ||
| self._offset_from_global = _offset_from_global or tuple(0 for _ in self.dimensions) | ||
| self._sprite_cache = {} | ||
@@ -256,2 +326,4 @@ def __repr__(self): | ||
| def _is_in_bounds(self, coord): | ||
| """Is a given coordinate within the space of this board? | ||
| """ | ||
| if len(coord) != len(self.dimensions): | ||
@@ -264,2 +336,5 @@ raise self.InvalidDimensionsError( | ||
| def _check_in_bounds(self, coord): | ||
| """If a given coordinate is not within the space of this baord, raise | ||
| an OutOfBoundsError | ||
| """ | ||
| if not self._is_in_bounds(coord): | ||
@@ -269,7 +344,11 @@ raise self.OutOfBoundsError("{} is out of bounds for {}".format(coord, self)) | ||
| def __contains__(self, coord): | ||
| """Implement <coord> in <board> | ||
| """ | ||
| return self._is_in_bounds(coord) | ||
| def __iter__(self): | ||
| """Iterator over all combinations of coordinates. If you need | ||
| data, use iterdata. | ||
| """Implement for <coord> in <board> | ||
| Iterate over all combinations of coordinates. If you need data, | ||
| use iterdata(). | ||
| """ | ||
@@ -301,3 +380,5 @@ # If all the dimensions are finite (the simplest and most common | ||
| def iterdata(self): | ||
| """Generate the list of data in local coordinate terms. | ||
| """Implement: for (<coord>, <data>) in <board> | ||
| Generate the list of data in local coordinate terms. | ||
| """ | ||
@@ -314,3 +395,3 @@ for gcoord, value in self._data.items(): | ||
| def iterline(self, coord, vector): | ||
| def iterline(self, coord, vector, max_steps=None): | ||
| """Generate coordinates starting at the given one and moving | ||
@@ -326,6 +407,27 @@ in the direction of the vector until the edge of the board is | ||
| raise InvalidDimensionsError() | ||
| n_steps = 0 | ||
| while self._is_in_bounds(coord): | ||
| yield coord | ||
| n_steps += 1 | ||
| if max_steps is not None and n_steps == max_steps: | ||
| break | ||
| coord = tuple(c + v for (c, v) in zip(coord, vector)) | ||
| def iterlinedata(self, coord, vector, max_steps=None): | ||
| """Use .iterline to generate the data starting at the given | ||
| coordinate and moving in the direction of the vector until | ||
| the edge of the board is reached or the maximum number of | ||
| steps has been taken (if specified). | ||
| This could be used, eg, to see whether you have a battleship | ||
| or a word in a word-search | ||
| """ | ||
| for coord in self.iterline(coord, vector, max_steps): | ||
| yield self[coord] | ||
| def corners(self): | ||
| dimension_bounds = [(0, len(d) -1 if d.is_finite else Infinity) for d in self.dimensions] | ||
| return list(itertools.product(*dimension_bounds)) | ||
| def copy(self, with_data=True): | ||
@@ -365,4 +467,9 @@ """Return a new board with the same dimensionality as the present one. | ||
| def __setitem__(self, coord, value): | ||
| coord = self._normalised_coord(coord) | ||
| self._data[coord] = value | ||
| if all(isinstance(c, (int, long)) for c in coord): | ||
| coord = self._normalised_coord(coord) | ||
| self._data[coord] = value | ||
| #~ elif all(isinstance(i, (int, long, slice)) for i in item): | ||
| #~ return self._slice(item) | ||
| else: | ||
| raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) | ||
@@ -453,2 +560,9 @@ def __delitem__(self, coord): | ||
| def occupied_board(self): | ||
| """Return a sub-board containing only the portion of this board | ||
| which contains data. | ||
| """ | ||
| (x0, y0), (x1, y1) = self.occupied() | ||
| return self[x0:x1+1, y0:y1+1] | ||
| def itercoords(self, coord1, coord2): | ||
@@ -463,29 +577,60 @@ """Iterate over the coordinates in between the two coordinates. | ||
| # | ||
| # TODO: what happens if the coords are reversed? | ||
| # | ||
| for coord in itertools.product(*(range(i1, 1 + i2) for (i1, i2) in zip(coord1, coord2))): | ||
| for coord in itertools.product(*(range(i1, 1 + i2) for (i1, i2) in zip(*sorted([coord1, coord2])))): | ||
| yield coord | ||
| def neighbours(self, coord): | ||
| """For a given coordinate, yield each of its nearest | ||
| neighbours along all dimensions. | ||
| def neighbours(self, coord, include_diagonals=True): | ||
| """Iterate over all the neighbours of a coordinate | ||
| For a given coordinate, yield each of its nearest neighbours along | ||
| all dimensions, including diagonal neighbours if requested (the default) | ||
| """ | ||
| gcoord = self._normalised_coord(coord) | ||
| offsets = itertools.product(*[(-1, 0, 1) for d in self.dimensions]) | ||
| for offset in offsets: | ||
| if all(o == 0 for o in offsets): | ||
| continue | ||
| # | ||
| # Diagonal offsets have no zero component | ||
| # | ||
| if include_diagonals or any(o == 0 for o in offset): | ||
| neighbour = tuple(c + o for (c, o) in zip(coord, offset)) | ||
| if self._is_in_bounds(neighbour): | ||
| yield neighbour | ||
| def runs_of_n(self, n, ignore_reversals=True): | ||
| """Iterate over all dimensions to yield runs of length n | ||
| Yield each run of n cells as a tuple of coordinates and a tuple | ||
| of data. If ignore_reversals is True (the default) then don't | ||
| yield the same line in the opposite direction. | ||
| This is useful for, eg, noughts and crosses, battleship or connect 4 | ||
| where the game engine has to detect a line of somethings in a row. | ||
| """ | ||
| all_zeroes = tuple(0 for _ in self.dimensions) | ||
| all_offsets = itertools.product(*[(-1, 0, 1) for d in self.dimensions]) | ||
| offsets = [o for o in all_offsets if o != all_zeroes] | ||
| already_seen = set() | ||
| # | ||
| # Find the bounding box for all coordinates surrounding coord. | ||
| # Then produce every coordinate in that space, selecting only | ||
| # those which fall onto the local board. | ||
| # This is brute force: running for every cell and looking in every | ||
| # direction. We check later whether we've run off the board (as | ||
| # the resulting line will fall short). We might do some kind of | ||
| # pre-check here, but we have to check against every direction | ||
| # of every dimension, which would complicate this code | ||
| # | ||
| mins = [x - 1 for x in coord] | ||
| maxs = [x + 1 for x in coord] | ||
| gcoords = set(c for c in itertools.product(*zip(mins, gcoord, maxs)) if self._is_in_bounds(c)) | ||
| # | ||
| # ... and remove the coordinate itself | ||
| # | ||
| gcoords.remove(coord) | ||
| for cell in iter(self): | ||
| for direction in offsets: | ||
| line = tuple(self.iterline(cell, direction, n)) | ||
| if len(line) == n: | ||
| if line in already_seen: | ||
| continue | ||
| already_seen.add(line) | ||
| # | ||
| # Most of the time you don't want the same line twice, | ||
| # once in each direction. | ||
| # | ||
| if ignore_reversals: | ||
| already_seen.add(line[::-1]) | ||
| for g in gcoords: | ||
| yield self._from_global(g) | ||
| yield line, [self[c] for c in line] | ||
@@ -502,9 +647,22 @@ def is_edge(self, coord): | ||
| def populate(self, iterable): | ||
| """Populate the entire board from an iterable | ||
| def is_corner(self, coord): | ||
| """Determine whether a position is on any corner of the board | ||
| The iterable can be shorter or longer than the board. The two | ||
| are zipped together so the population will stop when the shorter | ||
| is exhausted. | ||
| Infinite dimensions only have a lower edge (zero); finite dimensions | ||
| have a lower and an upper edge. | ||
| """ | ||
| self._check_in_bounds(coord) | ||
| dimension_bounds = ((0, len(d) - 1 if d.is_finite else 0) for d in self.dimensions) | ||
| return all(c in bounds for (c, bounds) in zip(coord, dimension_bounds)) | ||
| def populate(self, iterable, coord_iterable=None): | ||
| """Populate all or part of the board from an iterable | ||
| The population iterable can be shorter or longer than the board | ||
| iterable. The two are zipped together so the population will stop | ||
| when the shorter is exhausted. | ||
| If no iterable is supplied for cooordinates, the whole board is | ||
| populated. | ||
| This is a convenience method both to assist testing and also for, | ||
@@ -515,25 +673,121 @@ eg, games like Boggle or word-searches where the board must start | ||
| supplied. | ||
| With a coordinate iterable this could be used, for example, to combine | ||
| iterline and a list of objects to populate data on a Battleships board. | ||
| """ | ||
| for coord, value in zip(self, iter(iterable)): | ||
| if coord_iterable is None: | ||
| board_iter = iter(self) | ||
| else: | ||
| board_iter = iter(coord_iterable) | ||
| for coord, value in zip(board_iter, iter(iterable)): | ||
| self[coord] = value | ||
| def draw(self): | ||
| for line in self.drawn(): | ||
| def draw(self, callback=str, use_borders=True): | ||
| """Draw the board in a very simple text layout | ||
| By default data items are rendered as strings. If a different callback | ||
| is supplied, it is called with the data item and should return a string. | ||
| The idea is that items can be "hidden" from the board, or rendered | ||
| differently according to some state. Think of Battleships where the | ||
| same object can be hidden, revealed, or sunk. | ||
| """ | ||
| for line in self.drawn(callback, use_borders): | ||
| print(line) | ||
| def drawn(self): | ||
| if len(self.dimensions) != 2 or any(d.is_infinite for d in self.dimensions): | ||
| def drawn(self, callback=str, use_borders=True): | ||
| if len(self.dimensions) != 2 or self.has_infinite_dimensions: | ||
| raise self.BoardError("Can only draw a finite 2-dimensional board") | ||
| data = dict((coord, str(v)) for (coord, v) in self.iterdata()) | ||
| cell_width = len(max((str(v) for v in data.values()), key=len)) | ||
| corner, hedge, vedge = "+", "-", "|" | ||
| divider = (corner + (hedge * cell_width)) * len(self.dimensions[0]) + corner | ||
| data = dict((coord, callback(v)) for (coord, v) in self.iterdata()) | ||
| if data: | ||
| cell_w = len(max((v for v in data.values()), key=len)) | ||
| else: | ||
| cell_w = 1 | ||
| if use_borders: | ||
| corner, hedge, vedge = "+", "-", "|" | ||
| else: | ||
| corner = hedge = vedge = "" | ||
| divider = (corner + (hedge * cell_w)) * len(self.dimensions[0]) + corner | ||
| yield divider | ||
| if use_borders: yield divider | ||
| for y in self.dimensions[1]: | ||
| yield vedge + vedge.join(data.get((x, y), "").center(cell_width) for x in self.dimensions[0]) + vedge | ||
| yield divider | ||
| yield vedge + vedge.join(data.get((x, y), "").center(cell_w) for x in self.dimensions[0]) + vedge | ||
| if use_borders: yield divider | ||
| def painted(self, callback, size, background_colour, use_borders): | ||
| if not Image: | ||
| raise NotImplementedError("Painting is not available unless Pillow is installed") | ||
| if len(self.dimensions) != 2 or self.has_infinite_dimensions: | ||
| raise self.BoardError("Can only paint a finite 2-dimensional board") | ||
| # | ||
| # Construct a board of the requested size, containing | ||
| # cells sized equally to fit within the size for each | ||
| # of the two dimensions. Keep the border between them | ||
| # proportional to the overall image size | ||
| # | ||
| n_wide = len(self.dimensions[0]) | ||
| n_high = len(self.dimensions[1]) | ||
| image = Image.new("RGBA", size) | ||
| if use_borders: | ||
| h_border = image.height / 80 | ||
| v_border = image.width / 80 | ||
| else: | ||
| h_border = v_border = 0 | ||
| draw = ImageDraw.Draw(image) | ||
| drawable_w = image.width - (1 + n_wide) * h_border | ||
| cell_w = round(drawable_w / n_wide) | ||
| drawable_h = image.height - (1 + n_high) * v_border | ||
| cell_h = round(drawable_h / n_high) | ||
| for (x, y) in self: | ||
| obj = self[x, y] | ||
| # | ||
| # If the cell is empty: draw nothing | ||
| # Try to fetch the relevant sprite from the cache | ||
| # If the sprite is not cached, generate and cache it | ||
| # If the sprite is larger than the cell, crop it to the correct | ||
| # size, maintaining its centre | ||
| # | ||
| if obj is Empty: | ||
| sprite = None | ||
| else: | ||
| try: | ||
| sprite = self._sprite_cache[obj] | ||
| except KeyError: | ||
| sprite = self._sprite_cache[obj] = callback(obj, (cell_w, cell_h)) | ||
| if sprite.width > cell_w or sprite.height > cell_h: | ||
| box_x = (sprite.width - cell_w) / 2 | ||
| box_y = (sprite.height - cell_h) / 2 | ||
| sprite = sprite.crop((box_x, box_y, cell_w, cell_h)) | ||
| # | ||
| # Draw the cell and any sprite within it | ||
| # | ||
| cell_x = round(h_border + ((cell_w + h_border) * x)) | ||
| cell_y = round(v_border + ((cell_h + v_border) * y)) | ||
| draw.rectangle((cell_x, cell_y, cell_x + cell_w, cell_y + cell_h), fill=background_colour) | ||
| if sprite: | ||
| x_offset, y_offset = _centred_coord((cell_w, cell_h), sprite.size) | ||
| image.alpha_composite(sprite, (cell_x + x_offset, cell_y + y_offset)) | ||
| # | ||
| # Return the whole image as PNG-encoded bytes | ||
| # | ||
| f = io.BytesIO() | ||
| image.save(f, "PNG") | ||
| return f.getvalue() | ||
| def paint(self, filepath, callback=text_sprite(), size=(800, 800), background_colour="#ffffcc", use_borders=True): | ||
| with open(filepath, "wb") as f: | ||
| f.write(self.painted(callback, size, background_colour, use_borders)) | ||
| def cornerposts(dimensions): | ||
| for d in dimensions: | ||
| yield 0 | ||
| if d.is_finite: | ||
| yield len(d) | ||
| if __name__ == '__main__': | ||
| pass | ||
| pass |
+109
-24
@@ -1,10 +0,11 @@ | ||
| Metadata-Version: 1.1 | ||
| Metadata-Version: 1.2 | ||
| Name: board | ||
| Version: 0.0.0.post0 | ||
| Version: 1.0 | ||
| Summary: Standard Board mechanism for Dojo tasks | ||
| Home-page: https://github.com/graingert/dojo-board | ||
| Author: Thomas Grainger | ||
| Author-email: board@graingert.co.uk | ||
| 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 | ||
| Description-Content-Type: UNKNOWN | ||
| Description: Board Game for Python Dojos | ||
@@ -59,3 +60,5 @@ =========================== | ||
| infinite size. (So if you have, say, 3 infinite dimensions, you have | ||
| the basis for a Minecraft layout). Dimensions are zero-based. | ||
| 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. | ||
@@ -113,3 +116,2 @@ Cells on the board are accessed by item access, eg board[1, 2] or | ||
| To read, write and empty the data at a board position, use indexing:: | ||
@@ -124,2 +126,5 @@ | ||
| b1[-1, -1] = "*" | ||
| print(b1[2, 2]) # "*" | ||
| del b1[0, 0] | ||
@@ -149,15 +154,2 @@ print(b1[0, 0]) # <Empty> | ||
| 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() | ||
| To populate the board from an arbitrary iterator, use .populate:: | ||
@@ -189,3 +181,3 @@ | ||
| The length of the board is the product of its dimension lengths. If any | ||
| dimension is infinite, the board length is infinte. NB to find the | ||
| dimension is infinite, the board length is infinite. NB to find the | ||
| amount of data on the board, use lendata:: | ||
@@ -208,2 +200,11 @@ | ||
| 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:: | ||
@@ -222,3 +223,3 @@ | ||
| EXPERIMENTAL: To iterate over all the coords in the rectangular space between | ||
| To iterate over all the coords in the rectangular space between | ||
| two corners, use .itercoords:: | ||
@@ -229,3 +230,3 @@ | ||
| EXPERIMENTAL: To iterate over all the on-board positions from one point in a | ||
| To iterate over all the on-board positions from one point in a | ||
| particular direction, use .iterline:: | ||
@@ -240,2 +241,41 @@ | ||
| 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 | ||
@@ -261,2 +301,47 @@ ---------- | ||
| 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 | ||
@@ -263,0 +348,0 @@ ---------------------------- |
+102
-18
@@ -50,3 +50,5 @@ Board Game for Python Dojos | ||
| infinite size. (So if you have, say, 3 infinite dimensions, you have | ||
| the basis for a Minecraft layout). Dimensions are zero-based. | ||
| 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. | ||
@@ -104,3 +106,2 @@ Cells on the board are accessed by item access, eg board[1, 2] or | ||
| To read, write and empty the data at a board position, use indexing:: | ||
@@ -115,2 +116,5 @@ | ||
| b1[-1, -1] = "*" | ||
| print(b1[2, 2]) # "*" | ||
| del b1[0, 0] | ||
@@ -140,15 +144,2 @@ print(b1[0, 0]) # <Empty> | ||
| 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() | ||
| To populate the board from an arbitrary iterator, use .populate:: | ||
@@ -180,3 +171,3 @@ | ||
| The length of the board is the product of its dimension lengths. If any | ||
| dimension is infinite, the board length is infinte. NB to find the | ||
| dimension is infinite, the board length is infinite. NB to find the | ||
| amount of data on the board, use lendata:: | ||
@@ -199,2 +190,11 @@ | ||
| 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:: | ||
@@ -213,3 +213,3 @@ | ||
| EXPERIMENTAL: To iterate over all the coords in the rectangular space between | ||
| To iterate over all the coords in the rectangular space between | ||
| two corners, use .itercoords:: | ||
@@ -220,3 +220,3 @@ | ||
| EXPERIMENTAL: To iterate over all the on-board positions from one point in a | ||
| To iterate over all the on-board positions from one point in a | ||
| particular direction, use .iterline:: | ||
@@ -231,2 +231,41 @@ | ||
| 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 | ||
@@ -252,2 +291,47 @@ ---------- | ||
| 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 | ||
@@ -254,0 +338,0 @@ ---------------------------- |
+0
-0
@@ -0,0 +0,0 @@ [bdist_wheel] |
+4
-4
@@ -9,3 +9,3 @@ from setuptools import setup | ||
| name='board', | ||
| version='0.0.0.post0', | ||
| version='1.0', | ||
| description='Standard Board mechanism for Dojo tasks', | ||
@@ -15,6 +15,6 @@ long_description=readme + '\n\n' + history, | ||
| author_email='mail@timgolden.me.uk', | ||
| maintainer='Thomas Grainger', | ||
| maintainer_email='board@graingert.co.uk', | ||
| maintainer='Tim Golden', | ||
| maintainer_email='mail@timgolden.me.uk', | ||
| license="unlicensed", | ||
| url='https://github.com/graingert/dojo-board', | ||
| url='https://github.com/tjguk/dojo-board', | ||
| py_modules=['board'], | ||
@@ -21,0 +21,0 @@ classifiers=[ |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
68283
46.01%666
48%