"""ThudBoard - The Discworld Boardgame without rules Version 1.8 Copyright 2003, 2004, 2005, 2006, 2007 by Marc Boeren """ import os import copy import re from xml.dom import minidom import codecs import encodings.utf_8 import texts def minidom_gettext(nodelist): txt = "" for node in nodelist: if node.nodeType == node.TEXT_NODE: txt = txt + node.data return txt.strip() #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# class ThudMove(object): """A single move from a piece, including thudded opponents, optional comments A piece is either a Dwarf (d) or a Troll (T). If it is neither of these, it will be represented by a question mark (?). From- and to-positions are given in Board-coordinates (A1..P15), the thud-list is a list of such coordinates. In Koom Valley Thud, there is an extra piece, the Rock (R). These pieces are used in a ThudBattle. This ThudBattle will store extra information, such as the physical possibility of the move and if the rules were applied for this move (ensuring at least physical possibility). """ dwarf, troll, rock = 0, 1, 2 def __init__(self, piece, from_pos, to_pos, thud_list, possible=False, rules=True, comment=''): self.piece = piece self.from_pos = from_pos.upper() self.to_pos = to_pos.upper() self.thud_list = [thudded.upper() for thudded in thud_list] self.possible = possible self.rules = rules self.comment = comment def get_height(self): """Return the number of lines in a text-representation.""" lines = 1 if self.thud_list: lines+= 1 + (len(self.thud_list)-1)/4 return lines def __repr__(self): return self.str_save() def txt_move(self, savedstate=False): """Return a multi-line string containing the move as text. Example for a Troll move and a subsequent Thudding of two Dwarfs: ~ T J10 - L12 x L13 x M13 """ txt = prepend = [''] if not self.rules: txt = [" ~"] # rules not enforced if not self.possible: txt = [" %"] # impossible move (implies '~') if savedstate: prepend = [" >>>"] piece_txt = {-1:'?', self.dwarf:'d', self.troll:'T', self.rock:'R'}[self.piece] txt = prepend + txt txt+= [" %s %s - %s" % (piece_txt, self.from_pos.upper(), self.to_pos.upper())] counter = 0 for thudded in self.thud_list: if not counter: txt+=["\n"] + prepend + [" "] txt+= [" x %s" % thudded.upper()] counter = (counter+1)%4; return "".join(txt) def str_save(self): """Return a single-line text representation that can be used by load()""" txt = "".join(self.txt_move().split()) return txt def load_str(self, txt, comment=''): """Initialize the move from a text-representation generated by save() or loaded from a clipboard""" movepattern = "^([~|%%])?([D|T|R|?])?(%(coord)s)-(%(coord)s)((X%(coord)s)*)?$" % \ {'coord':"[A-HJ-P][1-9][0-5]?"} match = re.match(movepattern, txt.upper()) if not match: return False state, p, fp, tp, tl = match.group(1, 2, 3, 4, 5) piece_check = {None:-1, '?':-1, 'D':self.dwarf, 'T':self.troll, 'R':self.rock} if not p in piece_check: return False # note: rules and possible are not checked, they may be faked self.rules = not (state) self.possible = not (state and state=='%') if not self.possible: self.rules = False self.piece = piece_check[p] self.from_pos = fp self.to_pos = tp self.thud_list = [coord for coord in tl.split('X') if coord.strip()] self.comment = comment return True #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# class ThudBattle(object): """From a starting position you play a number of moves. The default Thud starting position is supplied, but you can override this with any prepared board setup. The list of moves is applied sequentially to the starting position to obtain the current position. This applying will stop as soon as an impossible move presents itself. You can view the history of the battle by setting the current position to different stages in the game (read: any move is a stage). New moves will be added after the current position, discarding any future moves. A new move is not allowed before the last saved move (the saved move is highlighted somehow). To do this anyway, you must go to some point in history and save the game from there. Note that a saved game will contain any future moves, although the saved move is remembered. The current move is stored in 'index'. The saved move is stored in 'saved_index'. """ name = texts.anonymousBattle battledir = '' dwarf, troll, rock = 0, 1, 2 start_position = [['F1', 'G1', 'J1', 'K1', 'L2', 'M3', 'N4', 'O5', 'P6', 'P7', 'P9', 'P10', 'O11', 'N12', 'M13', 'L14', 'K15', 'J15', 'G15', 'F15', 'E14', 'D13', 'C12', 'B11', 'A10', 'A9', 'A7', 'A6', 'B5', 'C4', 'D3', 'E2'], ['G7', 'H7', 'J7', 'G8', 'J8', 'G9', 'H9', 'J9'], ['H8'],] position = [[], [], []] moves = [] index = -1 saved_index = -1 comment = '' def __init__(self, name = None, start_pos = None, moves = None, index = -1, comment=''): self.init_positions() if name: self.name = name if start_pos: self.start_position = copy.deepcopy(start_pos) self.position = copy.deepcopy(self.start_position) if moves: self.moves = copy.deepcopy(moves) self.index = len(moves) - 1 if index: self.index = index self.get_position(index) self.saved_index = self.index self.filename = name self.comment = comment def init_positions(self): """Calculate valid and invalid coordinates.""" self.mapx = dict(zip(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'O', 'P'], range(15))) self.remapx = dict([(value, key) for key, value in self.mapx.items()]) self.invalid_position = [] for i in range(5): for j in range(5-i): self.invalid_position+= ["%s%d" % (self.remapx[j], 1+i), "%s%d" % (self.remapx[14-j], 1+i), "%s%d" % (self.remapx[j], 15-i), "%s%d" % (self.remapx[14-j], 15-i)] self.valid_position = [] for i in range(15): for j in range(15): position = "%s%d" % (self.remapx[j], 1+i) if position not in self.invalid_position: self.valid_position+= [position] def get_position(self, index): """Return the board configuration after move 'index' is played.""" self.position = copy.deepcopy(self.start_position) if index<0: self.index = -1 return self.position # notice the LAST played move is index, so this must # be included in the range for i in range(index+1): m = self.moves[i] if not self.update_position(m): self.index = i-1 return self.position self.index = index return self.position def update_position(self, thudmove): """Return the board configuration with the given move.""" if not self.check_move(thudmove): return False self.position[thudmove.piece].remove(thudmove.from_pos) for thudded in thudmove.thud_list: if thudded in self.position[self.dwarf]: self.position[self.dwarf].remove(thudded) if thudded in self.position[self.troll]: self.position[self.troll].remove(thudded) if thudded in self.position[self.rock]: self.position[self.rock].remove(thudded) self.position[thudmove.piece].append(thudmove.to_pos) return True def save_index(self): """Set the saved_index to the current index.""" self.saved_index = self.index def piece_on(self, coordinate): """Return the piece on the given coordinate, if any.""" if coordinate in self.position[self.dwarf]: return self.dwarf if coordinate in self.position[self.troll]: return self.troll if coordinate in self.position[self.rock]: return self.rock return -1 def add_move(self, piece, from_pos, to_pos, thud_list): """Alternative for add_thudmove.""" return self.add_thudmove(ThudMove(piece, from_pos, to_pos, thud_list, True, False)) def add_thudmove(self, thudmove): """Update the internals and the position with this move.""" if self.index=0 and index=base_index: h = self.get_height(index) lines-= h if lines < 0: return index index+= 1 return -1 def txt_battle_move(self): """Return a text-representation of the current move.""" return self.moves[self.index].txt_move() def txt_battle_moves(self, mutable_offset, maxlines=25, history_lines=0): """Return a list with the text-representation of the moves.""" from_index = mutable_offset[0] battle = [] for i in range(len(self.moves)): battle+= [self.moves[i].txt_move(self.saved_index==i)] if history_lines: while history_lines>1+"\n".join(battle[from_index:self.index+1]).count("\n"): from_index-=1 if from_index<=0: break if from_index<0: from_index = 0 mutable_offset[0] = from_index battlelines = "\n".join(battle[from_index:]).split("\n") return battlelines[0:maxlines] def txt_battle_positions(self, position): """Return a list with the text-representation of the position.""" battle = [] battle+= ["\nDwarves\n"] counter = 0 for dwarf in position[self.dwarf]: battle+= [" %-3s " % dwarf.lower()] counter = (counter+1)%8; if not counter: battle+="\n" battle+= ["\nTrolls\n"] counter = 0 for troll in position[self.troll]: battle+= [" %-3s " % troll.upper()] counter = (counter+1)%8; if not counter: battle+="\n" battle+= ["\Rock\n"] counter = 0 for rock in position[self.rock]: battle+= [" %-3s " % rock.upper()] counter = (counter+1)%8; if not counter: battle+="\n" return battle def txt_battle(self): """Return a string representing the entire battle.""" battle = ["\n\nBattle positions\n"] battle+= self.txt_battle_positions(self.start_position) battle+= ["\nBattle moves\n"] battle+= "\n".join(self.txt_battle_moves()) position = self.get_position(len(self.moves)) battle+= ["\n\nNew battle positions\n"] battle+= self.txt_battle_positions(position) return "".join(battle) def str_position(self, position): str = [] for piece in position[self.dwarf]: str+= ["d%s" % piece.upper()] for piece in position[self.troll]: str+= ["T%s" % piece.upper()] for piece in position[self.rock]: str+= ["R%s" % piece.upper()] return ",".join(str) def position_str(self, str): coordre = re.compile("[A-HJ-P][1-9][0-5]?") position = [[], [], []] pieces = str.split(",") for piece in pieces: p = piece[0] coord = piece[1:] if p not in ('d', 'T', 'R'): return False if p=='d': p = self.dwarf elif p=='T': p = self.troll else: p = self.rock if not coordre.match(coord): return False position[p].append(coord) return position def save_position(self, filename = None, index = -1): if not filename: filename = self.get_filename() try: f = file(filename, "wb") except: f = None if not f: return False f.write(self.str_position(self.get_position(index)) + "\n") f.close() return True def save(self, filename = None): if not filename: filename = self.get_filename() try: f = file(filename, "wb") except: f = None if not f: return False f.write(self.str_position(self.start_position) + "\n") for i in range(len(self.moves)): if self.index==i: f.write(">") f.write(self.moves[i].str_save() + "\n") f.close() self.filename = filename self.name = os.path.split(filename)[1] self.name = os.path.splitext(self.name)[0] self.save_info(filename + 'info') return True def load(self, filename): if not filename: filename = self.get_filename() try: f = file(filename, "rb") except: f = None if not f: return False lines = f.readlines() f.close() name = os.path.split(filename)[1] name = os.path.splitext(name)[0] start_position = self.position_str("".join(lines[0].split())) if not start_position: return False if not start_position[self.rock]: start_position[self.rock] = ['H8']; moves = [] index = -1 i = 0 for line in lines: if not i: i = 1 continue line = line.strip() if not len(line): continue # ignore empty lines moves.append(ThudMove(-1, "", "", [])) if line[0]==">": index = i-1 line = line[1:] if not moves[i-1].load_str("".join(line.split())): return False i+= 1 self.name = name self.filename = filename self.start_position = copy.deepcopy(start_position) self.position = copy.deepcopy(start_position) self.moves = copy.deepcopy(moves) self.index = index self.get_position(index) self.saved_index = self.index self.comment = '' self.load_info(filename + 'info') return True def get_filename(self): filename = self.filename if not filename: filename = self.name filename = os.path.join(self.battledir, filename+".thud") if not self.name or filename==texts.anonymousBattle: filename = os.path.join(self.battledir, "") return filename def has_comments(self): return self.comment or [True for move in self.moves if move.comment] def load_info(self, filename): try: dom = minidom.parse(filename) except: return False if dom.documentElement.tagName != 'thudbattle': dom.unlink() return False info = dom.getElementsByTagName('info') if info: comment = info[0].getElementsByTagName('comment') if comment: self.comment = minidom_gettext(comment[0].childNodes) movescontainer = dom.getElementsByTagName('moves') if movescontainer: moves = movescontainer[0].getElementsByTagName('move') if moves: i = 0; for move in moves: comment = move.getElementsByTagName('comment') if comment: try: self.moves[i].comment = minidom_gettext(comment[0].childNodes) except: pass i+= 1 dom.unlink() return True def save_info(self, filename): if not self.has_comments(): try: os.remove(filename) except: pass return True dom = minidom.parseString('') if self.comment: info = dom.getElementsByTagName('info') if info: comment = dom.createElement('comment') comment.appendChild(dom.createTextNode(self.comment)) info[0].appendChild(comment) if self.moves: movescontainer = dom.getElementsByTagName('moves') if movescontainer: for move in self.moves: moveelement = dom.createElement('move') if (move.comment): comment = dom.createElement('comment') comment.appendChild(dom.createTextNode(move.comment)) moveelement.appendChild(comment) movescontainer[0].appendChild(moveelement) try: f = file(filename, "wb") except: dom.unlink() f = None if not f: return False f.write(dom.toprettyxml(" ", "\n", "utf-8")) f.close() dom.unlink() return True #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# if __name__=='__main__': print "This file is not meant to be executed. Run thud.py instead."""