Source code for towers.core.towers

#!/usr/bin/env python
# -*- coding: latin-1 -*-
#
# @module towers.core.towers
# @version 0.1
# @copyright (c) 2017-present Francis Horsman.

import contextlib
import copy
import json
import math
from collections import Sequence

import six

from .errors import (
    InvalidEndingConditions, InvalidStartingConditions,
)
from .moves import Move
from .rod import Rod
from .rods import Rods
from .utils import Serializable
from .validation import (
    Validatable, validate_height, validate_moves, validate_rods,
)

__all__ = [
    'Towers',
]


[docs]class Towers(Sequence, Validatable, Serializable): """ A representation of the towers including all logic. """ class JsonEncoder(json.JSONEncoder): def default(self, obj): # pylint: disable=E0202 if isinstance(obj, Towers): return obj.to_json() else: # pragma: no cover return json.JSONEncoder.default(self, obj)
[docs] def __init__(self, height=1, rods=None, moves=0, verbose=False): """ :param int height: The height of the towers (ie: max number of disks each one rod can hold). :param Rods rods: An existing :class:`Rods` instance to use with this :class:`Towers` (the heights must match). :param int moves: The number of moves already taken. :param verbose: True=enable verbose logging mode. """ validate_height(height) validate_rods(rods) validate_moves(moves) self._rods = rods if rods is not None else Rods(height) self._moves = moves self._verbose = bool(verbose)
[docs] def to_json(self): """ Return a json serializable representation of this instance. :rtype: object """ return { 'height': self.height, 'verbose': self.verbose, 'moves': self.moves, 'rods': self._rods.to_json(), }
@classmethod
[docs] def from_json(cls, d): """ Return a class instance from a json serializable representation. :param str|dict d: The json or decoded-json from which to create a new instance. :rtype: Towers :raises: See :class:`Towers`.__new__. """ if isinstance(d, six.string_types): d = json.loads(d) return cls( height=d.pop('height'), verbose=d.pop('verbose'), moves=d.pop('moves'), rods=Rods.from_json(d.pop('rods')), )
@contextlib.contextmanager
[docs] def context(self, reset_on_success=True, reset_on_error=False): """ Create a temp context for performing moves. The state of this instance will be reset at context exit. :param bool reset_on_success: Reset this instance's state on exit from the context if no error occurred. Default = True. :param bool reset_on_error: Reset this instance's state on exit from the context if an error occurred. Default = False. """ self.validate_start() verbose = self.verbose moves = self.moves rods = self._rods.to_json() try: yield self self.validate_end() except Exception: # Error inside context or validation: if reset_on_error: self._verbose = verbose self._moves = moves self._rods = Rods.from_json(rods) else: if reset_on_success: self._verbose = verbose self._moves = moves self._rods = Rods.from_json(rods)
[docs] def __bool__(self): """ A Towers is considered True if it's state is completed. :rtype: bool """ return self.__nonzero__()
[docs] def __nonzero__(self): """ A Towers is considered non-zero if it's state is completed. :rtype: bool """ try: self.validate_end() return True except Exception: return False
[docs] def __copy__(self): """ Return a shallow copy of this instance. :rtype: :class:`Towers` """ return Towers( height=self.height, rods=self._rods, moves=self.moves, verbose=self.verbose, )
[docs] def __deepcopy__(self, *d): """ Return a deep copy of this instance. :param dict d: Memoisation dict. :rtype: :class:`Towers` """ return Towers.from_json(self.to_json())
[docs] def __eq__(self, other): """ Compare Towers instances for equivalence. :param Towers other: The other :class:`Towers` to compare. :rtype: bool """ if isinstance(other, Towers): if other.height == self.height: if other._rods == self._rods: return True
[docs] def __getitem__(self, index): """ Get the :class:`Rod` at the given index. :param int index: The index to get the :class:`Rod` at. :rtype: Rod """ return self._rods[index]
[docs] def __contains__(self, x): """ Does this :class:`Towers` contain the given :class:`Rod`. :param Rod x: The :class:`Rod` to find. :rtype: bool """ if isinstance(x, Rod): return x in self._rods
[docs] def __len__(self): """ Determine how many :class:`Rod`'s this :class:`Towers` contains. :rtype: int """ return len(self._rods)
[docs] def __iter__(self): """ Run the towers, yielding :class:`Move` instances. """ for i in self.move_tower( height=self.height, start=self.start_rod, end=self.end_rod, tmp=self.tmp_rod, ): yield i
def __str__(self): return 'Towers({rods})'.format(rods=self._rods) @property def verbose(self): """ Obtain this instance's verbose flag. :rtype: bool """ return self._verbose @verbose.setter def verbose(self, verbose): """ Set this instance's verbose flag :param object verbose: True=enable verbose logging mode. """ self._verbose = bool(verbose) @property def moves(self): """ Determine how many moves have occurred so far. :rtype: int """ return self._moves @property def height(self): """ Obtain the height of the :class:`Towers` (ie: max number of disks each one rod can hold). :rtype: int """ return self._rods.height @staticmethod
[docs] def moves_for_height(height): """ Determine the max number of moves required to solve the puzzle for the given height :param int height: The height of the :class:`Rods` (number of :class:`Disk` on a :class:`Rod`). :rtype: int """ return int(math.pow(2, height)) - 1
[docs] def validate_start(self): """ Validate the start conditions for this towers :raises InvalidTowerHeight: The height of the :class:`Towers` is invalid :raises DuplicateDisk: This :class:`Rod` already contains this :class:`Disk`. :raises CorruptRod: A :class:`Disk` is on top of a :class:`Disk` of smaller size. :raises InvalidStartingConditions: Initial conditions are invalid. """ validate_height(self.height) self._rods.validate() if not (bool(self._rods.start) and not bool(self._rods.end)): raise InvalidStartingConditions(self._rods, self.moves) if self.moves != 0: raise InvalidStartingConditions(self._rods, self.moves)
[docs] def validate_end(self): """ Validate the end conditions for this towers. :raises InvalidTowerHeight: The height of the tower is invalid :raises DuplicateDisk: This :class:`Rod` already contains this :class:`Disk`. :raises CorruptRod: A :class:`Disk` is on top of a :class:`Disk` of smaller size. :raises InvalidEndingConditions: End conditions are invalid. """ validate_height(self.height) self._rods.validate() if not (bool(self._rods.end) and not bool(self._rods.start)): raise InvalidEndingConditions(self._rods)
[docs] def validate(self): """ Perform self validation. :raises InvalidTowerHeight: The height of the tower is invalid :raises DuplicateDisk: This :class:`Rod` already contains this :class:`Disk`. :raises CorruptRod: A :class:`Disk` is on top of a :class:`Disk` of smaller size. """ validate_height(self.height) self._rods.validate()
[docs] def __enter__(self): """ Context-Manager entry, validate our entry state for towers-start conditions. :raises: See :func:`Towers.validate_start`. """ self.validate_start()
[docs] def __exit__(self, *args, **kwargs): """ Context-Manager exit, validate our exit state for towers-end conditions. :raises: See :func:`Towers.validate_end`. """ self.validate_end()
[docs] def __call__(self): """ Run the towers. Convenience method. :raises: See :func:`Towers.move_tower`. """ for i in self: if self.verbose: print(i)
@property def start_rod(self): """ Retrieve the start :class:`Rod` for this towers. :rtype: Rod """ return self._rods.start @property def end_rod(self): """ Retrieve the end :class:`Rod` for this towers. :rtype: Rod """ return self._rods.end @property def tmp_rod(self): """ Retrieve the temporary :class:`Rod` for this towers. :rtype: Rod """ return self._rods.tmp
[docs] def move_tower(self, height, start, end, tmp): """ Move the stack of `Disks` on a `Rod`. :param int height: The height of the :class:`Disk` to move. :param Rod start: The :class:`Rod` to move the :class:`Disk` from. :param Rod end: The :class:`Rod` to move the :class:`Disk` to. :param Rod tmp: The intermediary :class:`Rod` to use when moving the :class:`Disk`. """ if height >= 1: for i in self.move_tower(height - 1, start, tmp, end): yield i for i in self.move_disk(start, end): yield i for i in self.move_tower(height - 1, tmp, end, start): yield i elif height == 1: for i in self.move_disk(start, end): yield i
[docs] def move_disk(self, start, end): """ Move the `Disk` from one Rod to another. :note: Generator, yields `Move` instances. :param Rod start: The :class:`Rod` to remove the :class:`Disk` from. :param Rod end: The :class:`Rods` to move the :class:`Disk` to. """ start_rod = copy.deepcopy(start) end_rod = copy.deepcopy(end) moves = self.moves disk = start.pop() move = Move(disk, start_rod, end_rod, moves) end.append(disk) self._moves += 1 yield move