#!/usr/bin/env python
# -*- coding: ISO-8859-1 -*-
"""\
Very simple game of checkers.
"""

_copyright = "(c) 2003 Gregor Raýman - www.grayman.de"

# This program is open source released under the BSD license
_license = """\
Copyright (c) 2003, Gregor Rayman - www.grayman.de"
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are 
permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice, this list 
  of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice, this list
  of conditions and the following disclaimer in the documentation and/or other materials 
  provided with the distribution.

- Neither the name of Gregor Rayman nor the names of its contributors may be used to 
  endorse or promote products derived from this software without specific prior written 
  permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &AS IS& AND ANY EXPRESS 
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

_version = "0.1"
_build = "$Id: checkers.py,v 1.7 2003/10/03 23:52:40 gra Exp $"

import random

BOARD_SIZE = 8
INITIAL_ROWS = 3
RED = 1
WHITE = 2

_otherColor = {RED: WHITE, WHITE: RED}

class InvalidMove:
    def __init__(self, move):
        self.move = move

class Board:

    def __init__(self, size = BOARD_SIZE):
        # empty fields
        self.fields = []
        self.size = size
        for i in range(self.size):
            row = [None] * self.size
            self.fields.append(row)
        #initialize statistics
        self.counts = {RED: [0, 0], WHITE: [0, 0]}
        # place the pieces
        for i in range(self.size // 2):
            for j in range(INITIAL_ROWS):
                Piece(self, RED, (j, i * 2 + j % 2))
                Piece(self, WHITE, (self.size - j - 1, i * 2 + (self.size - j - 1) % 2))
        # history
        self.history = []
        # turn
        self.turn = RED
        self.upsidedown = False
    def __str__old(self):
        result = ''
        rowindex = range(self.size - 1, -1, -1)
        colindex = range(self.size)
        if self.upsidedown:
            rowindex, colindex = colindex, rowindex

        for r in rowindex:
            rowstr = '%d: ' % (r + 1)
            for c in colindex:
                p = self.fields[r][c]
                if p is None: rowstr += (r+c)%2 and ' ' or '.'
                else: rowstr += str(p)
                rowstr += ' '
            result += rowstr + '\n'
        rowstr = '   '
        for c in colindex:
            rowstr += '%c ' % (ord('a') + c)
        result += rowstr
        return result
    def __str__(self):
        # converts the board to the display coordinates
        if self.upsidedown:
            boardtodisp = lambda c: self.size - c - 1
        else:
            boardtodisp = lambda c: c
        # empty board without borders
        line1 = list(' .  ' * (self.size // 2) + ' .' * (self.size % 2) + ' ')
        line2 = list('   .' * (self.size // 2) + '  ' * (self.size % 2) + ' ')
        display = []
        for i in range(self.size):
            display.append(line1[:])
            line1, line2 = line2, line1

        # display the last move
        if self.history:
            move = self.history[-1]
            rr, cc = move.steps[0]
            for r, c in move.steps:
                if abs(r-rr) == 2 or abs(c-cc) == 2:
                    display[boardtodisp((r+rr)//2)][boardtodisp((c+cc)//2) * 2 + 1] = '*'
                display[boardtodisp(r)][boardtodisp(c) * 2 + 1] = '*'
                rr, cc = r, c
            display[boardtodisp(r)][boardtodisp(c) * 2] = '('
            display[boardtodisp(r)][boardtodisp(c) * 2 + 2] = ')'

        # place the stones
        for r, row in enumerate(self.fields):
            for c, p in enumerate(row):
                if p:
                     display[boardtodisp(r)][boardtodisp(c) * 2 + 1] = str(p)

        # make 1st line bottom
        display.reverse()

        # add labels and concatenate to string
        result = ''
        for i, line in enumerate(display):
            result += "%d: " % (boardtodisp(self.size - i - 1) + 1) + ''.join(line) + '\n'
        result += '    '
        for i in range(self.size):
            result += '%c ' % (boardtodisp(i) + ord('a'))
        return result

    def suggest(self, gaze):
        suggestion = self.getBestMoves(gaze)
        if suggestion is None:
            return None
        score, moves = suggestion
        return score, random.choice(moves)

    def undo(self):
        if (self.history):
            self.history[-1].revert()

    def getPossibleMoves(self):
        allMoves = []
        jumps = False
        for row in self.fields:
            for piece in row:
                if not piece is None:
                    jump, moves = piece.getPossibleMoves()
                    if jump and not jumps:
                        jumps = True
                        allMoves = []
                    if jump or not jumps:
                        allMoves += moves
        return allMoves

    def getMoveScore(self, move, gaze):
        try:
            move.applyto(self)
            possible = self.getPossibleMoves() # possible moves of the opponent

            if gaze <= 0:
                return self._score() + (len(possible),)
            else:
                if not possible: # opponent lost

                    return (1000, 1000, 1000, 1000)
                nextGaze = gaze - len(possible)
                bestNextScore = (-10000, -10000, -10000, -10000)
                for nextMove in possible:
                    nextScore = self.getMoveScore(nextMove, nextGaze)
                    if nextScore > bestNextScore:
                        bestNextScore = nextScore
                return -bestNextScore[0], -bestNextScore[1], -bestNextScore[2], -bestNextScore[3]
        finally:
            self.undo()

    def getBestMoves(self, gaze):
        possible = self.getPossibleMoves()
        if not possible:
            return None
        if len(possible) == 1:
            return (0,0,0,1), possible # score does not matter, we have no choice

        best = []
        bestScore = (-1000, -1000, -1000, -1000)
        for move in possible:
            score = self.getMoveScore(move, gaze)
            c = cmp(score, bestScore)
            if c > 0:
                best = [move]
                bestScore = score
            elif c == 0:
                best.append(move)
        return bestScore, best

    def _score(self):
        "returns the score from the point of view of the next player"
        totalDiff = self.counts[_otherColor[self.turn]][0] - self.counts[self.turn][0]
        queenDiff = self.counts[_otherColor[self.turn]][1] - self.counts[self.turn][1]
        total = self.counts[RED][0] + self.counts[WHITE][0]
        if totalDiff < 0:
            return totalDiff, queenDiff, total
        if totalDiff > 0:
            return totalDiff, queenDiff, -total
        if queenDiff < 0:
            return totalDiff, queenDiff, total
        if queenDiff > 0:
            return totalDiff, queenDiff, -total
        return totalDiff, queenDiff, 0

    def _check(self):
        for r, row in enumerate(self.fields):
            for c, field in enumerate(row):
                if not field is None:
                    assert(field.coordinates == (r, c))

class Piece:
    def __init__(self, board, color, coordinates):
        self.board = board
        self.color = color
        self.coordinates = coordinates
        self.queen = False
        self.onboard = True
        self.__mark = False
        assert(board.fields[coordinates[0]][coordinates[1]] is None)
        board.fields[coordinates[0]][coordinates[1]] = self
        board.counts[color][0] += 1
    def promote(self):
        assert(not self.queen and self.onboard)
        self.queen = True
        self.board.counts[self.color][1] += 1
    def demote(self):
        assert(self.queen and self.onboard)
        self.queen = False
        self.board.counts[self.color][1] -= 1
    def moveto(self, coordinates):
        self._check()
        assert(self.board.fields[coordinates[0]][coordinates[1]] is None)
        self.board.fields[self.coordinates[0]][self.coordinates[1]] = None
        self.coordinates = coordinates
        self.board.fields[self.coordinates[0]][self.coordinates[1]] = self
        self._check()
    def remove(self):
        self._check()
        assert(self.onboard)
        self.board.counts[self.color][0] -= 1
        if self.queen: self.board.counts[self.color][1] -= 1
        self.board.fields[self.coordinates[0]][self.coordinates[1]] = None
        self.onboard = False
        self._check()
    def putback(self):
        self._check()
        assert(not self.onboard)
        self.board.counts[self.color][0] += 1
        if self.queen: self.board.counts[self.color][1] += 1
        assert(self.board.fields[self.coordinates[0]][self.coordinates[1]] == None)
        self.board.fields[self.coordinates[0]][self.coordinates[1]] = self
        self.onboard = True
        self._check()
    def __str__(self):
        if self.color == RED:
            return self.queen and 'R' or 'r'
        return self.queen and 'W' or 'w'
    def __repr__(self):
        return "%c%d" % (ord('a') + self.coordinates[1], 1 + self.coordinates[0]) + str(self)

    def _check(self):
        if self.onboard:
            assert(self.board.fields[self.coordinates[0]][self.coordinates[1]] == self)
    def getPossibleMoves(self):
        if self.color != self.board.turn:
            return False, []
        if self.queen:
            directions = ((1, 1), (-1, 1), (1, -1), (-1, -1)) # all 4 directions

        elif self.color == RED:
            directions = ((1, 1), (1, -1)) # only increasing row numbers

        else:
            directions = ((-1, 1), (-1, -1)) # only decreasing row numbers


        # look for jumps
        moves = self._getPossibleJumps(directions)
        if moves:
            return True, moves # some jumps found, other moves not allowed


        for dr, dc in directions:
            r, c = self.coordinates[0]+dr, self.coordinates[1]+dc
            if (0 <= r < self.board.size) and (0 <= c < self.board.size):
                p = self.board.fields[r][c]
                if p is None:
                    moves.append(Move([self.coordinates, (r, c)]))

        return False, moves # simple moves (list might be empty)


    def _getPossibleJumpsRec(self, directions, jumps, pos, steps):
        found = False
        for dr, dc in directions:
            tr, tc = pos[0] + dr, pos[1] + dc # taken

            r, c = tr + dr, tc + dc # pos after jump

            if (0 <= r < self.board.size) and (0 <= c < self.board.size):
                pt = self.board.fields[tr][tc]
                p = self.board.fields[r][c]
                if (p is None and (not pt is None and pt.color != self.color and not pt.__mark)):
                    pt.__mark = True
                    steps.append((r, c))
                    try:
                        if not self._getPossibleJumpsRec(directions, jumps, (r, c), steps):
                            jumps.append(Move(steps[:])) # copy of current path

                            found = True
                    finally:
                        steps.pop()
                        pt.__mark = False
        return found

    def _getPossibleJumps(self, directions):
        jumps = []
        steps = [self.coordinates]
        self._getPossibleJumpsRec(directions, jumps, self.coordinates, steps)
        return jumps

class Move:
    def __init__(self, steps):
        self.steps = steps
        self.applied = False
    def __repr__(self):
        dash = ''
        result = ''
        for r, c in self.steps:
            result += dash + "%c%d" % (ord('a') + c, 1 + r)
            # dash = " - "
        return result
    def applyto(self, board):
        "Applies the move."
        self.piece = board.fields[self.steps[0][0]][self.steps[0][1]]
        if self.piece is None:
            raise InvalidMove(str(self))
        # Move the piece
        self.piece.moveto(self.steps[-1])
        # Promote to queen if necessary
        if self.piece.coordinates[0] in (0, self.piece.board.size -1) and not self.piece.queen:
            self.promotes = True
            self.piece.promote()
        else:
            self.promotes = False
        # Remove taken pieces
        self.taken = []
        rr, cc = self.steps[0]
        for r, c in self.steps[1:]:
            tr, tc = (r+rr) // 2, (c+cc) // 2
            if (tr != r and tr != rr):
                self.taken.append(self.piece.board.fields[tr][tc])
                self.piece.board.fields[tr][tc].remove()
            rr, cc = r, c
        # write itself in history and switch color
        self.piece.board.history.append(self)
        self.piece.board.turn = _otherColor[self.piece.color]
        self.applied = True
    def revert(self):
        "Reverts the move. Moves the pieces back and puts back the taken pieces"
        assert(self.applied)
        assert(self.piece.board.history[-1] == self)
        self.piece.board.history.pop()
        self.piece.board.turn = self.piece.color
        # Put back taken pieces
        for p in self.taken:
            p.putback()
        # Demote queen if promoted
        if self.promotes:
            self.piece.demote()
        # Move it back
        self.piece.moveto(self.steps[0])
        self.applied = False
        del self.piece

import cmd

class Game(cmd.Cmd):
    def __init__(self, difficulty=20, completekey='tab', stdin=None, stdout=None):
        cmd.Cmd.__init__(self, completekey, stdin, stdout)
        difficulty = difficulty < 5 and 5 or difficulty > 25 and 25 or difficulty
        self.difficulty = difficulty
    def preloop(self):
        self.board = Board()
        self.stdout.write("Welcome to the game\n\n")
        self.stdout.write(str(self.board))
        self.stdout.write("\n\n")
    def emptyline(self):
        pass
    def do_play(self, move):
        possible = self.board.getPossibleMoves()
        matching = [m for m in possible if str(m).startswith(move)]
        if 1 == len(matching):
            matching[0].applyto(self.board)
            self.stdout.write(str(self.board))
            self.stdout.write("\n\n")
            self.computerplay()
        elif len(matching):
            self.stdout.write("Which move did you mean?\n")
            for m in matching:
                self.stdout.write("  " + str(m) + "\n")
        else:
            self.stdout.write("InvalidMove!\n")
            self.stdout.write("Possible moves are:\n")
            for m in possible:
                self.stdout.write("  " + str(m) + "\n")
    def help_play(self):
        self.stdout.write("""\
Play a move.

The move has to be specified in the form <letter><number>...<letter><number>.
Example c3d4. The move does not have to be specified completely, it is enough
to enter as much as is needed to identify the possible move.

Example:
When the game starts, the only possible move from to column "a" is "a3b4", so
in the case only "a" has to be entered. If only one move is possible, only the
word "play" has to be entered. Otherwise is the word "play" optional.

If an invalid move is entered, or if the move is not identified by the entry,
a list of all allowed moves will be printed.
""")
    def computerplay(self):
        suggestion = self.board.suggest(20)
        if suggestion is None:
            self.stdout.write("Game is over\n")
            return True
        else:
            suggestion[1].applyto(self.board)
        self.stdout.write("\n\n" + str(suggestion[1]) + "\n\n")
        self.stdout.write(str(self.board))
        self.stdout.write("\n\n")
    def do_swap(self, dummy = None):
        self.board.upsidedown = not self.board.upsidedown
        self.computerplay()
    def help_swap(self):
        self.stdout.write("Swaps the sides\n")
    def do_undo(self, dummy = None):
        self.board.undo()
        self.board.undo()
        self.stdout.write(str(self.board))
        self.stdout.write("\n\n")
    def help_undo(self):
        self.stdout.write("Undo the last move\n")
    def do_quit(self, dummy = None):
        return True
    def help_quit(self):
        self.stdout.write("Quit the game\n")
    def do_license(self, dummy = None):
        self.help_license()
    def help_license(self):
        self.stdout.write(_license)
    def default(self, line):
        if line.strip():
            self.do_play(line)

if "__main__" == __name__:
    g = Game()
    g.cmdloop()