def __init__(self, thread_id, connection): super().__init__() self._connection = connection self._connected = True self.connection_open = EventHook() self.connection_closed = EventHook() self.end_character = bytes(chr(93), 'UTF-8') self.message_received = EventHook() self.start_character = bytes(chr(91), 'UTF-8') self.thread_id = thread_id self._filter_tree = None
def __init__(self, player1, board1, player2, board2): self._currentPlayerBoard = (player1, board1) self._otherPlayerBoard = (player2, board2) #wall, attack, link and fusion counting fields self.numWall = 0 self.numAttack = 0 self.numLink = 0 self.numFusion = 0 #event emitters self.switchTurn = EventHook() #event handlers board1.wallMade.addHandler(self._wallMade) board2.wallMade.addHandler(self._wallMade) board1.attackMade.addHandler(self._attackMade) board2.attackMade.addHandler(self._attackMade) board1.fusionMade.addHandler(self._fusionMade) board2.fusionMade.addHandler(self._fusionMade) player1.justDied.addHandler(self._playerJustDied) player2.justDied.addHandler(self._playerJustDied) # When board1 decides it's time to attack, it will call # damageCalculate on board2. Then board2's attackReceived event # will trigger with the details of the attack; we listen to it # to see if the player took damage. board1.attackNow.addHandler(board2.damageCalculate) board2.attackReceived.addHandler(self._attackReceived) # And the same for the other board. board2.attackNow.addHandler(board1.damageCalculate) board1.attackReceived.addHandler(self._attackReceived)
def __init__(self): # event broadcasters - registered listeners persist after game reset # so they are initialized in __init__ rather than init self.onChoiceChange = EventHook() # called when choiceList changes self.onGameModeChange = EventHook() # called when gameMode changes self.onAddLangNode = EventHook( ) # called when addLangNode is called self.onCharacterSwitch = EventHook( ) # called when activeProtagonistInd changes self.onEnterArea = EventHook() # called when enterArea is called self.onEnterRoom = EventHook() # called when enterRoom is called self.onClearBuffer = EventHook( ) # called when clearBuffer is called self.onSettingChange = EventHook() # called when a setting changes self.onInventoryChange = EventHook( ) # called when inventory changes self.init()
def __init__(self, height, width): # The grid is an array of pointers to pieces. The same # piece may be pointed to from multiple squares (for big pieces). # If an entry is None, there is nothing in that square. # Orientation: row 0 = middle of board, height = the hero's belly # (0,0) = bottom left, going up. (view from second player's). self.grid = np.ndarray((height, width), dtype=object) # A set of pieces on the board. self.units = set() # Dimensions (in logical squares) of the board. self.height = height self.width = width #Set of attack formations self.currentAttacks = set() # Event handlers that will be triggered each time a piece is # changed (added, removed, or moved). The callbacks should take # one unnamed argument, which is a set of pieces to which they apply. # Pieces that have been removed will have position set to None. self.pieceUpdated = EventHook() # The set of pieces that have been updated since the last time # the pieceUpdated handler was triggered. self._updatedPieces = set() # Keeps track of piece positions so that we can detect if they # have changed. self._piecePositions = {} # Event handler that will be triggered each time # when an attack formation is created self.attackMade = EventHook() # Event handler that will be triggered each time # when a wall is created self.wallMade = EventHook() # Event handler that will be triggered when fusion is made # TODO. NOT IMPLEMENTED self.fusionMade = EventHook() # Event handler that will be triggered when units attack self.attackNow = EventHook() # Event handler that will be triggered when an emeny unit attacks. # The argument to the event handler is a list of AttackSummary. self.attackReceived = EventHook() # Event handler that will be triggered when the turn begins, #after all currentAttacks have been updated self.turnBegun = EventHook()
def __init__(self, description, unitFactory, baseWeights=[], baseNames=[], specialWeights=[], specialNames=[], specialRarity=[]): """ Parameters: baseWeights: 3 non-negative numbers sum to 3 baseNames: 3 names of base units, possibly with repeats. manaFactor: dict. Whenever a player does a mana-generating action (eg: link), the amount of mana they get for it will be multiplied by the number specified maxUnitTotal: max total number of units the player can call manaFactor: factors to be used in calculations. Choices are 1, 1.2, 1.5, 2 higher factor means mana fills up faster when making links/fusions/etc.. maxLife: maximum life maxMoves: max number of moves per turn wall: dictionary of wall properties. Include image toughness maxToughness """ self.description = description self.name = description.get('name', '') self.maxLife = int(description['maxLife']) self.maxMoves = int(description['maxMoves']) self.maxMana = int(description['maxMana']) self.maxUnitTotal = int(description['maxUnitTotal']) self.race = description['race'] self.wallDescription = description['wall'] self.manaFactor = {} for key, value in description['manaFactor'].items(): self.manaFactor[key] = int(value) #weight distribution of base units. Integers, sum to 3 self.baseWeights = np.array(baseWeights) self.baseNames = baseNames self.baseColor = ['red', 'white', 'blue'] #weight distribution of special unit type. Integers, sum to 10 self.specialWeights = np.array(specialWeights) self.specialNames = specialNames #how rare a special unit is. 10 = common, 5 = uncommon, 3 = rare self.specialRarity = np.array(specialRarity) self.unitFactory = unitFactory #set effective params self._mana = 0 self._life = self.maxLife self._usedMoves = 0 self._calledUnit = 0 self._calledFatties = 0 # After losing special units, there is a temporary penalty to # its weight; effWeights keeps track of the possibly penalized value. self.effWeights = self.specialWeights.copy() #effective unit weights # Event emitters self.doneTurn = EventHook() self.justDied = EventHook() # Callbacks take a single argument that is the new value self.manaChanged = EventHook() self.lifeChanged = EventHook() self.moveChanged = EventHook() self.unitChanged = EventHook()
class Player(object): """ Model class. Has player data: life, number of moves, special equip, champions available, mana, Has unit generating function. """ def __init__(self, description, unitFactory, baseWeights=[], baseNames=[], specialWeights=[], specialNames=[], specialRarity=[]): """ Parameters: baseWeights: 3 non-negative numbers sum to 3 baseNames: 3 names of base units, possibly with repeats. manaFactor: dict. Whenever a player does a mana-generating action (eg: link), the amount of mana they get for it will be multiplied by the number specified maxUnitTotal: max total number of units the player can call manaFactor: factors to be used in calculations. Choices are 1, 1.2, 1.5, 2 higher factor means mana fills up faster when making links/fusions/etc.. maxLife: maximum life maxMoves: max number of moves per turn wall: dictionary of wall properties. Include image toughness maxToughness """ self.description = description self.name = description.get('name', '') self.maxLife = int(description['maxLife']) self.maxMoves = int(description['maxMoves']) self.maxMana = int(description['maxMana']) self.maxUnitTotal = int(description['maxUnitTotal']) self.race = description['race'] self.wallDescription = description['wall'] self.manaFactor = {} for key, value in description['manaFactor'].items(): self.manaFactor[key] = int(value) #weight distribution of base units. Integers, sum to 3 self.baseWeights = np.array(baseWeights) self.baseNames = baseNames self.baseColor = ['red', 'white', 'blue'] #weight distribution of special unit type. Integers, sum to 10 self.specialWeights = np.array(specialWeights) self.specialNames = specialNames #how rare a special unit is. 10 = common, 5 = uncommon, 3 = rare self.specialRarity = np.array(specialRarity) self.unitFactory = unitFactory #set effective params self._mana = 0 self._life = self.maxLife self._usedMoves = 0 self._calledUnit = 0 self._calledFatties = 0 # After losing special units, there is a temporary penalty to # its weight; effWeights keeps track of the possibly penalized value. self.effWeights = self.specialWeights.copy() #effective unit weights # Event emitters self.doneTurn = EventHook() self.justDied = EventHook() # Callbacks take a single argument that is the new value self.manaChanged = EventHook() self.lifeChanged = EventHook() self.moveChanged = EventHook() self.unitChanged = EventHook() @property def mana(self): return self._mana @mana.setter def mana(self, m): m = min(m, self.maxMana) self._mana = m self.manaChanged.callHandlers(m) @property def usedMoves(self): return self._usedMoves @usedMoves.setter def usedMoves(self, u): self._usedMoves = u self.moveChanged.callHandlers(u) @property def life(self): return self._life @life.setter def life(self, l): delta = l - self._life self._life = l logging.debug('updated life to %d' % self._life) self.lifeChanged.callHandlers(l) if delta < 0: #loss of life contributes towards increase in mana self.mana = self.mana - delta if self._life <= 0: #if dead self.justDied.callHandlers() @property def calledUnit(self): return self._calledUnit @calledUnit.setter def calledUnit(self, u): self._calledUnit = u self.unitChanged.callHandlers(u) def setGameManager(self, gameManager): self.gameManager = gameManager def getSpecialUnit(self): """ Return a special unit. Relative probability of specific types is innerproduct(effweight, specialRarity). The color is chosen proportionally to baseWeight """ effOdds = self.specialRarity * self.effWeights weights = [np.random.uniform()*x for x in effOdds] unitName = self.specialNames[np.argmax(weights)] colorweights = [np.random.uniform()*x for x in self.baseWeights] unit = self.unitFactory.create(unitName, self.baseColor[np.argmax(colorweights)], player=self) return unit def getBaseUnit(self): """ Return a base unit. Relative probability of specific types is baseWeights """ weights = [np.random.uniform()*x for x in self.baseWeights] i = np.argmax(weights) unitName = self.baseNames[i] unit = self.unitFactory.create(unitName, self.baseColor[i], player = self) return unit def getRandomUnit(self, enemyFatties): """ Return a random unit Probability of being special is 0.1*innerproduct(effWeights, specialRarity)/100* max((enemyFatties+1)/(self._calledFatties+1), 1) """ specialOdds = self.specialRarity * self.effWeights fattyBoost = max((1. + enemyFatties)/(1 + self._calledFatties), 1.) if np.random.uniform() < 0.1*specialOdds/100*fattyBoost: unit = self.getSpecialUnit() else: unit = self.getBaseUnit() return unit
class Board: """Represents one player's board. """ def __init__(self, height, width): # The grid is an array of pointers to pieces. The same # piece may be pointed to from multiple squares (for big pieces). # If an entry is None, there is nothing in that square. # Orientation: row 0 = middle of board, height = the hero's belly # (0,0) = bottom left, going up. (view from second player's). self.grid = np.ndarray((height, width), dtype=object) # A set of pieces on the board. self.units = set() # Dimensions (in logical squares) of the board. self.height = height self.width = width #Set of attack formations self.currentAttacks = set() # Event handlers that will be triggered each time a piece is # changed (added, removed, or moved). The callbacks should take # one unnamed argument, which is a set of pieces to which they apply. # Pieces that have been removed will have position set to None. self.pieceUpdated = EventHook() # The set of pieces that have been updated since the last time # the pieceUpdated handler was triggered. self._updatedPieces = set() # Keeps track of piece positions so that we can detect if they # have changed. self._piecePositions = {} # Event handler that will be triggered each time # when an attack formation is created self.attackMade = EventHook() # Event handler that will be triggered each time # when a wall is created self.wallMade = EventHook() # Event handler that will be triggered when fusion is made # TODO. NOT IMPLEMENTED self.fusionMade = EventHook() # Event handler that will be triggered when units attack self.attackNow = EventHook() # Event handler that will be triggered when an emeny unit attacks. # The argument to the event handler is a list of AttackSummary. self.attackReceived = EventHook() # Event handler that will be triggered when the turn begins, #after all currentAttacks have been updated self.turnBegun = EventHook() def __getitem__(self, item): '''Return the corresponding sub-table of grid. Throws an error if index out of bound ''' i, j = item if isinstance(i, tuple) or isinstance(i, list): imax = max(i) else: imax = i if isinstance(j, tuple) or isinstance(j, list): jmax = max(j) else: jmax = j if(imax < self.grid.shape[0] and jmax < self.grid.shape[1]): return self.grid[i,j] else: return None def getPieces(self, rows, cols): """ Return a LIST of pieces on board in range [rows] x [cols] remove None and redundancies . Items scanned by column then by row, in the input order """ units = list() for j in cols: for i in rows: if self[i,j] is not None: if self[i,j] not in units: units.append(self[i,j]) return units def __setitem__(self, item, unit): """ make grid at location item points to unit """ i,j = item if isinstance(i, tuple) or isinstance(i, list): imax = max(i) else: imax = i if isinstance(j, tuple) or isinstance(j, list): jmax = max(j) else: jmax = j if imax >= self.grid.shape[0] or jmax >= self.grid.shape[1]: raise IndexError("Index larger than board size") self.grid[i,j] = unit @property def boardHeight(self): """Returns the first empty row in each column. The return value is a numpy array, with one entry per column.""" heightA = np.zeros(self.grid.shape[1], dtype = 'int8') for j in range(self.grid.shape[1]): for i in reversed(range(self.grid.shape[0])): if self[i,j] != None: heightA[j] = i+1 break return heightA def normalize(self): """Updates the board by sliding pieces by priority, find and update links, and iterate these 2 steps until no more updates required. """ madeStuff = 1 while(madeStuff > 0): madeStuff = 0 self._shiftByPriority() #shift higher priority guys to front self._reportPieceUpdates() create = self._createFormations() #check and make new formations self._reportPieceUpdates() #if created new things, need to shift by priority again madeStuff += create createWall = self._mergeWalls() #if created walls madeStuff += createWall self._reportPieceUpdates() def _reportPieceUpdates(self): """Trigger pieceUpdated events. The pieces to be updated are everything that is currently in self._updatedPieces, together with the pieces that have changed their position since we were last called. """ moved = set() for u in self.units: if u in self._piecePositions and self._piecePositions[u] != u.position: u.oldPosition = self._piecePositions[u] moved.add(u) self._updatedPieces.update(moved) if self._updatedPieces: # Pass a copy, so that it isn't cleared when we clear our version. self.pieceUpdated.callHandlers(self._updatedPieces.copy()) self._updatedPieces.clear() # Record the current positions of all pieces, for use the next time # we get called. self._piecePositions.clear() for u in self.units: # Copy the list, in case the original changes. self._piecePositions[u] = list(u.position) def _mergeWalls(self): """Merges pairs of vertically adjacent walls. Does one merging per column, from bottom row to top ALSO: merge units that can be merged (such as fusing) Returns true if anything changed. """ #cycle through units by column over (j), then over row (i) updated = False for j in range(self.grid.shape[1]): for i in range(self.boardHeight[j]): unit = self[i,j] if unit is not None: unitTop = self[i+unit.size[0],j] if unitTop and unit.canMerge(unitTop): updated = True self._deleteFromGrid(unitTop) unit.merge(unitTop) self._deletePiece(unitTop) break return updated def _shiftByPriority(self): """Shift pieces by priority. Return true if anything changed. """ #piece.slidePriority: integer. higher = should be more at the bottom. #sort by (row,col) in increasing order, then by priority #unitList = sorted(unitList, key = lambda piece: piece.slidePriority, reverse = True) updated = False nrow = self.grid.shape[0] ncol = self.grid.shape[1] #keep a fatty list fatty = set() #sort the pieces on the board one column at a time by priority for j in range(ncol): #for each column #get the units in column j unitCol = self.getPieces(range(nrow), [j]) if not unitCol: #column j has no unit continue #sort by decreasing priority unitCol = sorted(unitCol, key = lambda piece: piece.slidePriority, reverse = True) #copy this over to the board self[range(nrow), j] = None i = 0 for unit in unitCol: self[range(i, i+unit.size[0]), j] = unit # If in the correct reference column, check if the unit has moved if i != unit.position[0] and j == unit.position[1]: updated = True unit.position[0] = i if unit.size == (2, 2): fatty.add(unit) #check the next height level i += unit.size[0] #check for fatty disalignment trynum = 0 while self._doAlignFatty(fatty): trynum = trynum + 1 logging.debug( 'Done fatty alignment trial number %s' % trynum) self.dumpPosition() if(trynum > 100): logging.error('shiftByPriority attempting to realign fatty over 100 times') raise IndexError("Attempted to realign fatty over 100 times") return updated def _unitIsHere(self, unit, pos): """Returns true if unit.position is pos, and board corresponding squares point to unit.""" if unit.position != pos: return False for i in range(0, unit.size[0]): for j in range(0, unit.size[1]): if self[pos[0] + i, pos[1]+j] != unit: return False return True def _doAlignFatty(self, fatList): """ correct guys in the fatList if unaligned these guys have to have size 2x2 return True if did something """ updated = False for unit in fatList: if not self._unitIsHere(unit, unit.position): #if not aligned logging.debug("Fatty named %s at position %s is not aligned." % (unit.name, str(unit.position))) #flag updated as True since we'll need another iteration updated = True #look up where the top corner is in the column j = unit.position[1]+1 topCornerLoc = None for i in reversed(range(self.grid.shape[0])): if self[i, j] == unit: topCornerLoc = i break #if top corner is higher logging.debug("His current top corner is in row %s. Expected row is %s" % (topCornerLoc, unit.position[0]+1)) if topCornerLoc > unit.position[0]+1: #shift the leftside up as far as possible delta = topCornerLoc - (unit.position[0]+1) maxShift = 0 for i in range(delta+1): if(self._canShiftUp(j-1,unit.position[0]+1+i, topCornerLoc)): maxShift = i #shift up by maxshift logging.debug("We need to shift his leftside up by %s. We can shift by %s" % (delta, maxShift)) if(maxShift > 0): shifted = self._doShiftUp(j-1, unit.position[0], unit.position[0]+maxShift) logging.debug("Successfully shifted: %s" % shifted) if maxShift == 0 or not shifted: #if cannot shift the left side up, then shift the rightside down logging.debug("Cannot shift the left side up. Shifting the right side down") self._doShiftDown(j, topCornerLoc-1,unit.position[0], 2) #if top corner is lower if topCornerLoc < unit.position[0] +1: #shift the rightside up as far as possible delta = unit.position[0]+1 - topCornerLoc maxShift = 0 for i in range(delta+1): if(self._canShiftUp(j, topCornerLoc-1, topCornerLoc-1+maxShift)): maxShift = i #shift up by maxshift logging.debug("We need to shift his rightside up by %s. We can shift by %s" % (delta, maxShift)) if(maxShift > 0): shifted = self._doShiftUp(j, topCornerLoc-1, topCornerLoc-1+maxShift) logging.debug("Successfully shifted: %s" % shifted) if maxShift == 0 or not shifted: #then shift the leftside down logging.debug("Cannot shift right side up. Shifting the left side down") self._doShiftDown(j-1,unit.position[0], topCornerLoc+maxShift,2) if self._unitIsHere(unit, unit.position): logging.debug('Successfully realigned fatty to position %s' % str(unit.position)) else: logging.debug('Alignment not successful for fatty at position %s' % str(unit.position)) return updated def _canShiftUp(self, col, oldRow, newRow): """ return True if in column col, can shift item from position oldRow to newRow (by making None's and move the objects behind one up) """ #check how many empty squares there are behind empty = 0 for i in range(oldRow, self.grid.shape[0]): if self[i, col] is None: empty += 1 if newRow - oldRow <= empty: #if shift up by an amount < empty: return True else: return False def _findBlockSize(self, col, oldRow): """Returns the size of the continuous block starting at (oldRow,col)""" size = 0 for i in range(oldRow, self.grid.shape[0]): if self[i,col] is None: return size else: size += 1 return size def _doShiftUp(self, col, oldRow, newRow): """ Shift an object at (oldRow, col) to (newRow, col), create None in the intermediate rows, and shift all the guys behind him as well. Assuming that things behind him are in a continuous column. Shift into empty squares if possible. Return True if did some changes """ shifted = False delta = newRow - oldRow while(delta > 0): #find the size of the continuous block blockSize = self._findBlockSize(col, oldRow) if blockSize == 0: delta -=1 oldRow += 1 else: #shift the empty cell down. Equivalently, shift #the current block up by one. self._doShiftDown(col, oldRow + blockSize, oldRow, 1) delta -= 1 oldRow += 1 shifted = True return shifted def _doShiftDown(self, col, oldRow, newRow, size): """ Shift an object of size size, from (oldRow, col) DOWN to (newRow, col). Requires newRow < oldRow For all objects which get displaced, put them in the same order in the vacant rows. Effectively: swap the blocks """ if(oldRow >= self.grid.shape[0]): raise IndexError("try shifting blocks outside of the board down") #identify current top and bottom blocks topBlock = self[range(oldRow, oldRow+size), col] botBlock = self[range(newRow, oldRow), col] #copy bottom out bottemp = botBlock.copy() #shift top down self[range(newRow, newRow+size), col] = topBlock #update unit position if its base is in this column for i in reversed(range(newRow, newRow+size)): unit = self[i,col] if unit is None: continue if unit.position[1] == col: unit.position[0] = i #shift bottom block up self[range(newRow+size, oldRow+size), col] = bottemp #update unit position if its base is in this column for i in reversed(range(newRow+size, oldRow+size)): unit = self[i,col] if unit is None: continue if unit.position[1] == col: unit.position[0] = i def rowToAdd(self, piece, col): """Returns the row at which the piece will be added, if it is added to the given column. If the returned value plus the height of the piece is larger than self.height, then the piece cannot be added in the given column. """ # If the piece belongs to the board, remove it from the # grid first, just in case it's already occupying the column # that we're adding it to. deleted = False if piece in self.units and self.grid[tuple(piece.position)] != None: deleted = True self._deleteFromGrid(piece) fat = piece.size[1] row = int(max(self.boardHeight[range(col, min(col+fat,self.width))])) if deleted: self._addToGrid(piece) return row def canAddPiece(self, piece, col): """Checks whether the given piece fits in the given column.""" tall = piece.size[0] fat = piece.size[1] if col + fat > self.grid.shape[1]: return False row = self.rowToAdd(piece, col) return row + tall <= self.grid.shape[0] def addPiece(self, piece, col): """Add a piece to the given column. Raise an IndexError if the piece doesn't fit at the given position. Note that this function does not normalize the board automatically. """ if not self.canAddPiece(piece, col): logging.error("Trying to add piece outside of board") raise IndexError("Trying to add piece outside of board") row = self.rowToAdd(piece, col) self._appearPiece(piece, [row, col]) def addPieceAtPosition(self, piece, row, col): """Add a piece at the given position. Raise an IndexError if the piece doesn't fit at the given position. Note that this function does not normalize the board automatically. """ try: self._appearPiece(piece, [row, col]) except ValueError: raise IndexError("Trying to appear piece in an invalid position.") def deletePiece(self, piece): """Delete a piece and then normalize.""" self._deletePiece(piece) self.normalize() def movePiece(self, piece, toColumn): """Moves a piece to a new column, then normalizes. Raises an IndexError if the move was illegal.""" if self.canAddPiece(piece, toColumn): self._deleteFromGrid(piece) self.addPiece(piece, toColumn) self.normalize() else: logging.error("Trying to add piece outside of board") def _deleteFromGrid(self, piece): """At the piece's current position, set the grid to None. Raises ValueError if the grid and the piece don't agree on where the piece is.""" tall = piece.size[0] fat = piece.size[1] row = piece.position[0] col = piece.position[1] # Raise an error if the piece thinks it's in a position that # actually belongs to another piece. (If the piece thinks it's # in a position that's actually empty, don't complain.) region = self.grid[row:(row+tall), col:(col+fat)].reshape(-1) if any([u != None and u != piece for u in region]): raise ValueError("Piece and board disagree on position", piece, piece.position) self.grid[row:(row+tall), col:(col+fat)] = None def _addToGrid(self, piece): """Add piece to the position supplied Raises ValueError if the position is already occupied.""" tall = piece.size[0] fat = piece.size[1] row = piece.position[0] col = piece.position[1] # We want to check if there are any non-None elements in # the region. Unfortunately, any(region != None) doesn't # work because the '!= None' comparison doesn't check # elementwise. region = self.grid[row:(row+tall), col:(col+fat)].reshape(-1) occupied = map(lambda x: x != None, region) if any(occupied): raise ValueError("Position is already occupied") self.grid[row:(row+tall), col:(col+fat)] = piece def _deletePiece(self, piece): """Remove a piece from the board. Raise a ValueError if the piece is not on the board. Note that this function does not normalize the board automatically. """ if piece not in self.units: raise ValueError("Tried to remove a non-existent piece") self.units.remove(piece) self._deleteFromGrid(piece) self._updatedPieces.add(piece) self.currentAttacks.discard(piece) piece.oldPosition = piece.position piece.position = None def _piecesInRegion(self, offset, regionSize): """The list of pieces in the rectangle of the given size at the given offset.""" row = offset[0] col = offset[1] return self.getPieces(range(row,(row+regionSize[0])), range(col,(col+regionSize[1]))) def _regionEmpty(self, offset, regionSize): return bool(self._piecesInRegion(offset, regionSize)) def _regionFull(self, offset, regionSize): """Checks whether the given region is full of pieces. Returns false if regionSize is 0 in either coordinate """ if min(regionSize) == 0: return False row = offset[0] col = offset[1] return (row + regionSize[0] <= self.height and col + regionSize[1] <= self.width and None not in self.grid[row:(row+regionSize[0]), col:(col+regionSize[1])].flatten().tolist()) # I'm not sure why flatten().tolist() is necessary, but it seems # like "None in array([None])" returns false, while None in [None] # returns true. def _chargers(self, piece): """The list of pieces in the given piece's charging region.""" # Add piece.size[0] because the charge region starts from the square # behind the piece. chargePosition = (piece.position[0] + piece.size[0], piece.position[1]) return self._piecesInRegion(chargePosition, piece.chargingRegion()) def _chargeFull(self, piece): """Checks whether the piece's charging region is full.""" chargePosition = (piece.position[0] + piece.size[0], piece.position[1]) return self._regionFull(chargePosition, piece.chargingRegion()) def _transformers(self, piece): """The list of pieces in the given piece's transform region.""" # Add piece.size[1] because the charge region starts from the square # to the right of the piece. transformPosition = (piece.position[0], piece.position[1] + piece.size[1]) return self._piecesInRegion(transformPosition, piece.transformingRegion()) def _transformFull(self, piece): """Checks whether the piece's transform region is full. """ transformPosition = (piece.position[0], piece.position[1] + piece.size[1]) return self._regionFull(transformPosition, piece.transformingRegion()) def _createFormations(self): """Create walls and charging formations. Return true if any formations were created.""" unitList = sorted(list(self.units), key = lambda piece: piece.position[0]) # a set of units to charge chargingPieces = set() # a set of units used as chargers chargerPieces = set() # a set of pieces to be transformed (into walls) transformingPieces = set() # counter for number of walls formed wallCount = 0 for unit in unitList: # Look at the pieces in the charging region. If there are some, # and they are all good then the unit gets charged. chargers = self._chargers(unit) if self._chargeFull(unit) and all(unit.canCharge(x) for x in chargers): chargingPieces.add(unit) chargerPieces.update(set(chargers)) #sort by increasing column (first key), and increasing row (second key) unitList = sorted(list(self.units), key = lambda piece: piece.position[0]) unitList = sorted(unitList, key = lambda piece: piece.position[1]) for unit in unitList: transformers = self._transformers(unit) if self._transformFull(unit) and all(unit.canTransform(x) for x in transformers): if unit not in transformingPieces: wallCount += 1 #mark the unit and _all_of its transformers as transforming transformingPieces.add(unit) transformingPieces.update(transformers) # Create charging formations. Resolve multiChargeable conflicts here. # To avoid update order problems, we sort chargingPieces by increasing column (first key) # and increasing row (second key) chargingPieces = sorted(list(chargingPieces), key = lambda piece: piece.position[0]) chargingPieces = sorted(chargingPieces, key = lambda piece: piece.position[1]) chargedPieces = [] # chargingChargers are pieces that are being charged and # being used to charge another piece. # transformingChargers are pieces that are transforming and # being used to charge another piece. transformingChargers = chargerPieces.intersection(transformingPieces) for unit in chargingPieces: # Ignore the unit if it has been removed from the board. # This could happen if it was used to charge something else. if unit in self.units: charged = unit.charge() chargedEndPosition = charged.position[0]+charged.size[0] chargers = set(self._chargers(unit)) # Delete all chargers unless the unit that was charged is # multichargeable. # If also transforming, make a ghost piece at the end of # the charged formation. The ghost acts as a placeholder # for the wall that will eventually go there. for x in chargers: if x in self.units: if x in transformingChargers: ghost = GhostPiece(x) ghost.position[0] = chargedEndPosition transformingPieces.add(ghost) transformingChargers.add(ghost) transformingPieces.remove(x) self._deletePiece(x) elif not unit._multiChargeable: self._deletePiece(x) else: if x not in chargingPieces: self._deletePiece(x) # If the unit is also transforming, create a ghost piece # at the end of the charging formation # and mark it as transforming if unit in transformingPieces: ghost = GhostPiece(unit) ghost.position[0] = chargedEndPosition transformingPieces.add(ghost) transformingChargers.add(ghost) transformingPieces.remove(unit) # Replace the piece with its charged version self._replacePiece(unit, charged) chargedPieces.append(charged) #only call handler if some charged pieces were made if len(chargedPieces) > 0: self.attackMade.callHandlers(set(chargedPieces)) #update the set of currentAttacks. self.currentAttacks.update(set(chargedPieces)) # Create walls. transformedPieces = [] for unit in transformingPieces: transformed = unit.transform() #Vanilla case: unit is not involved in charging. #Then, we will replace the unit with a wall at its current position if unit not in transformingChargers: self._replacePiece(unit, transformed) transformedPieces.append(transformed) # Case 2: Unit was used as a charger. By the time we get here, # it was replaced by a ghost whose current position # is just behind the charged formation. In this case, we try to # add a wall at the ghost's position while shifting everyone back # (while also ensuring that fatties fit). # If there is no room, we do not make the wall. if unit in transformingChargers: #check if we can shift items up oldRow = unit.position[0] newRow = unit.position[0]+1 col = unit.position[1] if self._canInsertPiece(unit, unit.position): #do the insertion. Leave fatty disalignment alone, it should be resolved by board normalize() self._doShiftUp(col, oldRow, newRow) self._appearPiece(transformed, unit.position) transformedPieces.append(transformed) #only call handler if some wall was made. if len(transformedPieces) > 0: self.wallMade.callHandlers([set(transformedPieces), wallCount]) return bool(chargedPieces or transformedPieces) def _canInsertPiece(self, piece, position): """ Return True if piece can be inserted to position (row, col) by shifting up pieces behind him And that fatty alignment is not perturbed. Return False otherwise. Used in the wall creation step of _createFormations. """ pHeight = piece.size[0] pWidth = piece.size[1] row = position[0] #check that there is room to shift up. for col in range(position[1], position[1]+pWidth): if not self._canShiftUp(col, row, row+pHeight): #if there is no room return False else: #check if fatty is messed up pieceList = set(self._piecesInRegion(offset = (row, col), regionSize = (self.height-row, 1))) for x in pieceList: if x.size == (2,2): #if the unit is a fatty #find his corner position fattyRow, fattyCol = x.position #if cannot shift the Fatty if not self._canShiftUp(fattyCol, fattyRow, fattyRow+pHeight): return False return True def _existPiece(self, piece): """ Return True if piece is existing on board at its stored position Only check the head square. """ return self.grid[piece.position] is piece def _appearPiece(self, piece, pos): """Place a new piece in the given position.""" self.units.add(piece) piece.position = pos self._addToGrid(piece) self._updatedPieces.add(piece) def _replacePiece(self, old, new): """Replaces an old piece with a new one.""" pos = old.position self._deletePiece(old) self._appearPiece(new, pos) def colToAdd(self, piece): """ Return the column value if the piece can be added without creating formations/walls. Return None if cannot be added anywhere. """ #logging.debug(str([u.position for u in self.units])) # choose a random ordering of the columns columnList = list(np.random.permutation(self.width)) for col in columnList: # Make a copy of the current board, then add a copy of the # piece in the current column. If no formations are created, # then the column is ok. if self.canAddPiece(piece, col): boardCopy = self.ghostBoard() ghost = GhostPiece(piece) boardCopy.addPiece(ghost, col) if not boardCopy._createFormations(): return col return None def ghostBoard(self): """ Return a hard copy of board with ghost pieces """ boardCopy = Board(self.height, self.width) for u in self.units: ghost = GhostPiece(u) boardCopy._appearPiece(ghost, u.position) return boardCopy def selfConsistent(self): """ Return true if board and units agree on their positions""" for u in self.units: row,col = u.position uheight, uwidth = u.size #check all squares that this unit should occupy for i in range(row, row + uheight): for j in range(col, col + uwidth): if self[i,j] != u: return False return True def beginTurn(self): """ This function is called by gameManager at the start of this board's turn. All charging units are updated. If there are units attacking this turn, the board emits the attackNow event, passing the set of attacking units. """ attackGuys = set() for x in self.currentAttacks: x.update() if x.readyToAttack(): attackGuys.add(x) # Remove the attackGuys from the list of currentAttacks. self.currentAttacks.difference_update(attackGuys) self.turnBegun.callHandlers() if len(attackGuys) > 0: # Send off the attackGuys to eventHandlers. self.attackNow.callHandlers(set(attackGuys)) # Now, we remove the attackGuys from the board. for x in attackGuys: self._deletePiece(x) self.normalize() def damageCalculate(self, attackEnemies): """ Handle damage calculations done on this board by attackEnemies """ # TODO: this doesn't currently define the order of attacking units. summaries = [] for enemy in attackEnemies: col = enemy.column # Find all the units in front of the enemy. defendUnits = self._piecesInRegion(offset = [0,col], regionSize = [self.height, enemy.width]) defendUnits = sorted(list(defendUnits), key = lambda piece: piece.row) # TODO: if there are two units at the same height, and there # is not enough strength to kill both of them, then the damage # should be split. enemyDead = False summary = AttackSummary(enemy) summaries.append(summary) for defender in defendUnits: oldDefToughness = defender.toughness oldEneToughness = enemy.toughness defender.toughness, defenderDead = defender.damage(enemy.toughness) enemy.toughness, enemyDead = enemy.damage(oldDefToughness) damage = oldEneToughness - enemy.toughness summary.add(defender, damage, defenderDead) if defenderDead: self._deletePiece(defender) if enemyDead: break # No more attacking if not enemyDead: # Any leftover damage goes to the player. summary.add(None, enemy.toughness, False) self.attackReceived.callHandlers(summaries) self._reportPieceUpdates() def dumpPosition(self): ''''This prints out the board and the sizes of the units. Sorted by columns ''' unitSorted = sorted(self.units, key = lambda unit: unit.position[1]) print str([(u.position, str(u.size)) for u in unitSorted])
class GameManager(object): """Runs the game. Keeps track of whose turn it is and how many moves they have left. Also manages the summoning of pieces. Events: switchTurn: triggered whenever a player ends their turn. Events handled: wallMade, chargeMade, and fusionMade from board: this information is used to make free moves and update mana. """ def __init__(self, player1, board1, player2, board2): self._currentPlayerBoard = (player1, board1) self._otherPlayerBoard = (player2, board2) #wall, attack, link and fusion counting fields self.numWall = 0 self.numAttack = 0 self.numLink = 0 self.numFusion = 0 #event emitters self.switchTurn = EventHook() #event handlers board1.wallMade.addHandler(self._wallMade) board2.wallMade.addHandler(self._wallMade) board1.attackMade.addHandler(self._attackMade) board2.attackMade.addHandler(self._attackMade) board1.fusionMade.addHandler(self._fusionMade) board2.fusionMade.addHandler(self._fusionMade) player1.justDied.addHandler(self._playerJustDied) player2.justDied.addHandler(self._playerJustDied) # When board1 decides it's time to attack, it will call # damageCalculate on board2. Then board2's attackReceived event # will trigger with the details of the attack; we listen to it # to see if the player took damage. board1.attackNow.addHandler(board2.damageCalculate) board2.attackReceived.addHandler(self._attackReceived) # And the same for the other board. board2.attackNow.addHandler(board1.damageCalculate) board1.attackReceived.addHandler(self._attackReceived) @property def currentPlayer(self): return self._currentPlayerBoard[0] @property def currentBoard(self): return self._currentPlayerBoard[1] @property def otherPlayer(self): return self._otherPlayerBoard[0] @property def otherBoard(self): return self._otherPlayerBoard[1] def movePiece(self, piece, col): """Puts piece to column col. Also recalculates the number of turns that the current player has left, and switches players if necessary. Returns True if the move succeeded, and False if it was an illegal move. """ if piece is None: return False if piece.position[1] == col: #dropping in the same position return False if self.currentBoard.canAddPiece(piece, col): self.currentBoard.movePiece(piece, col) self._updateMoves() return True else: return False def deletePiece(self, position): """Deletes the piece at the given position. Also recalculates the number of turns that the current player has left, and switches players if necessary. """ if self.currentBoard[position] is not None: logging.debug("Deleting the piece %s at position %s" % (self.currentBoard[position], position)) self.currentBoard.deletePiece(self.currentBoard[position]) self._updateMoves(offset = 0) else: logging.debug("Tried to delete an empty square %s" % position) def canPickUp(self, position): """Checks if the current player can pick up a given piece.""" pass def endTurn(self): """Ends the active player's turn. If the player has moves left, increases that player's mana. Toggles the current player. Triggers the switchTurn event. """ moveLeft = self.currentPlayer.maxMoves - self.currentPlayer.usedMoves if moveLeft > 0: self._updateMana("move", moveLeft) self.currentPlayer.usedMoves = 0 #reset number of moves tmp = self._currentPlayerBoard self._currentPlayerBoard = self._otherPlayerBoard self._otherPlayerBoard = tmp self.switchTurn.callHandlers() #begin the new turn self.currentBoard.beginTurn() def _wallMade(self, walls): """Count the number of walls made in board """ self.numWall += walls[1] def _attackMade(self, newAttacks): """Count number of attacks and links made in board """ self.numAttack += len(newAttacks) #count links: iterate over all charging formations, find friends oldAttacks = self.currentBoard.currentAttacks for unit in newAttacks: if self._countLinks(unit, oldAttacks.union(newAttacks)) > 0: self.numLink += 1 def _updateMana(self, evt, num): """ Update currentPlayer's mana depend on the event triggered. Event types: "link", "fuse", "move", "useMana" """ if evt == "useMana": # We should only be here if the mana is full. self.currentPlayer.mana = 0 else: logging.debug('updating mana for event "%s" x%s' % (evt, num)) factor = 0 try: factor = self.currentPlayer.manaFactor[evt] except KeyError: logging.error('player description missing manaFactor ' + evt) logging.debug('increment is %s' % (factor * num)) self.currentPlayer.mana += factor * num logging.debug('new mana is %s' % self.currentPlayer.mana) def useMana(self): """ Use mana if allowed. Return True if can use, False otherwise """ if self.currentPlayer.mana >= self.currentPlayer.maxMana: self._updateMana("useMana", 0) return True return False def _fusionMade(self, fusion): """Count number of fusions """ self.numFusion += len(fusion) def _countLinks(self, unit, unitList): """Return the number of links for a unit. The number of links for a unit is the number of other charging units that have the same color and will attack on the same turn. A unit isn't counted as a link for itself.""" linkNum = 0 unitList.discard(unit) for poozer in unitList: if (poozer.color == unit.color) and (poozer.turn == unit.turn): linkNum += 1 return linkNum def _updateMoves(self, offset = 1): """ Computes the new number of moves, including free moves earned. Update mana, number of units can call, reset parameters, and ends turn if no moves left. offset = 1 by default, 0 if came from deleting pieces """ self.currentPlayer.usedMoves += 1 if self.numWall > 0 or self.numAttack > 0: freeMoves = self.numWall + self.numAttack - offset self.currentPlayer.usedMoves -= freeMoves #update mana for moves self._updateMana("move", freeMoves) #reset numWall and numAttack self.numWall = 0 self.numAttack = 0 self._updateMana("link", self.numLink) self.numLink = 0 self._updateMana("fuse", self.numFusion) self.numFusion = 0 self.currentPlayer.calledUnit = len(self.currentBoard.units) if self.currentPlayer.usedMoves == self.currentPlayer.maxMoves: self.endTurn() def _attackReceived(self, summaries): """Someone was just attacked (but not necessarily damaged). Check if the attack got through. """ for summary in summaries: if summary.attacks: attack = summary.attacks[-1] logging.debug(attack.defender) # None is the sentinal value meaning the attack got through to the player. if attack.defender is None: self.otherPlayer.life -= attack.damageDealt #TODO def _playerJustDied(self): """Player just died event handler.""" logging.debug("Player %s just died" % self.otherPlayer.name) pass def callPieces(self): """Current player wants to call some pieces. Generate units one at a time, and check that they can be fit on board. Chance of getting fatties is coupled with the opponent's number of fatties ever generated Debugging mode: store the configuration of the board after generated. """ pieceLeft = self.currentPlayer.maxUnitTotal - len(self.currentBoard.units) logging.debug('Calling %d pieces for player %s' % (pieceLeft, str(self.currentPlayer))) addedPieces = pieceLeft > 0 retries = 10 while pieceLeft > 0: unit = self.currentPlayer.getRandomUnit(self.otherPlayer._calledFatties) col = self.currentBoard.colToAdd(unit) if col is None: logging.warning('A piece with dimensions %d x %d did not fit on the board' % (unit.size[0], unit.size[1])) retries -= 1 if(retries == 0): logging.debug('Board is full while there are %d pieces left to call' % pieceLeft) break else: self.currentBoard.addPiece(unit, col) pieceLeft = pieceLeft - 1 if(unit.size == (2,2)): self.currentPlayer._calledFatties += 1 # Assuming colToAdd works correctly, normalizing the board # should not actually change anything. However, we need to call # it to make sure listeners get notified of all the new pieces. self.currentBoard.normalize() if addedPieces: self._updateMoves()
class SocketClient(threading.Thread): def __init__(self, thread_id, connection): super().__init__() self._connection = connection self._connected = True self.connection_open = EventHook() self.connection_closed = EventHook() self.end_character = bytes(chr(93), 'UTF-8') self.message_received = EventHook() self.start_character = bytes(chr(91), 'UTF-8') self.thread_id = thread_id self._filter_tree = None def destined_for_me(self, socket_message): # Using the filter set, determine if the message is destined for the client. visitor = filter_message_visitor(socket_message) return visitor.visit(self._filter_tree) def disconnect(self): # Close the connection and fire the connection closed event. self._connection.close() self.connection_closed.fire(self) def run(self): self.set_filter("1") # Fire the connection open event. self.connection_open.fire(self) # Try to read from the socket connection. try: read_buffer = bytearray() # Read from the connection until instructed not to. while self._connected: # Read a singular byte from the socket. socket_byte = self._connection.recv(1) # If nothing was read from the socket. The socket likely closed. if not socket_byte: self.disconnect() return if socket_byte == self.start_character: # If the byte read is the start character. Clear the buffer. read_buffer = bytearray() elif socket_byte == self.end_character: # If the byte read is the end character. Send the message. self.message_received.fire(self, read_buffer.decode('UTF-8')) read_buffer = bytearray() else: # Any other character simply append to the buffer. read_buffer.extend(socket_byte) # If there is an OS error exit. except OSError: pass def set_filter(self, filter_message): # Set the filter for the client. Use the new filter to build a parse tree. input_string = InputStream(filter_message) lexer = filter_messageLexer(input_string) stream = CommonTokenStream(lexer) parser = filter_messageParser(stream) self._filter_tree = parser.prog() def send_message(self, message): # Translate the message to bytes and send the message. bytes_to_send = bytes(str(message), 'UTF-8') self._connection.send(bytes_to_send)