#!/usr/bin/env python# encoding: utf-8"""The hexbattle game logic.It is completely based on hexmaps and should be(-come) mostlyindependent of the type of user interaction and presentation."""fromcoreimportcore,Sprite,runfromos.pathimportjoinfrompyglet.clockimportschedule_interval,schedule_once# hex functionsfromhexgridimport*# characters and overlaysfromhexbattle_unitsimport*classResult:def__init__(self,name,data=None,description=None,commands=[]):"""The result of the action."""self.name=nameself.data=dataself.description=descriptionself.commands=commands# often used case of having only one command.iflen(commands)==1:self.command=commands[0]else:self.command=Nonedef__repr__(self):returnself.__class__.__name__+"(\""+self.name+"\", data="+str(self.data)+", description="+str(self.description)+", commands="+str(self.commands)+")"classCommand:def__init__(self,fun,name,**arguments):"""A single command with a function, a name as identifier and arguments with the possible values in a tuple."""self.fun=funself.name=nameself.arguments=argumentsdef__call__(self,*args,**kwds):# the args are only there to catch dt=… from schedule.returnself.fun(**kwds)def__repr__(self):returnself.__class__.__name__+"(\""+repr(self.fun)+"\", "+str(self.name)+",".join(self.arguments)+")"classTerrainTile:"""A tile of ground with properties."""def__init__(self,imagepath):self.image=imagepathclassModel:"""The basic game logic. It only knows about hexfields."""def__init__(self):self.hex_vectors=hex_vectors#: Tiles of the ground with properties: {hex-vector: TerrainTile()}self.terrain={}#: the results of the current actionself.results=None#: The possible level positions. self.hexmap={}#: stuff which should be shown by a Ui.self.visible=[]#: The current phase of the game.self.phase={}# Player States: CPU (computer decision), wait (animation), setup (setup of the scene, noninteractive).self.phase["player"]="setup"#: All units that can actself.chars=[]# settingsself.player_team="trees"self.num_rats=10self.num_goblins=7defare_enemies(self,unit,other):"""Check if the given units are enemies."""returnunit.teamisNoneorother.teamisNoneornotunit.team==other.teamdefunits_around_hex(self,x,y):"""Return the units around the hexfield."""units=[]forhex_x,hex_yinself.hex_vectors[1:]:other=self.hexmap.get((hex_x+x,hex_y+y),None)ifotherisnotNone:units.append(other)returnunitsdeffree_hex_around(self,x,y):"""Return the units around the hexfield."""hexes=[]forhex_x,hex_yinself.hex_vectors[1:]:pos=hex_x+x,hex_y+yother=self.hexmap.get(pos,None)ifotherisNone:hexes.append(pos)returnhexesdefenemies_around(self,unit):"""Return the enemies which are on hexes touching the hex of the unit (hexfield). @param unit: A Charakter with at least the attributes group, hex_x and hex_y."""enemies=[]forotherinself.units_around_hex(unit.hex_x,unit.hex_y):ifotherisnotNoneandself.are_enemies(unit,other):enemies.append(other)returnenemiesdefall_enemies_of(self,unit):"""Get all enemies of the unit, regardless of their position."""return[cforcinself.hexmap.values()ifself.are_enemies(unit,c)]deffind_free_hex(self,start_x,start_y):"""Find the next free hexfield."""x,y=start_x,start_yvectors=set(self.hex_vectors)#: make sure only the new vector parts are tested.new_vectors=vectors.copy()whileTrue:fordx,dyinnew_vectors:pos=(x+dx,y+dy)ifnotposinself.hexmaporself.hexmap[pos]isNone:returnposnew_vectors_tmp=set()# add vectors around the new vectors new vectorsfordx,dyinself.hex_vectors:forddx,ddyinnew_vectors:# never check hexes twicepos=(dx+ddx,dy+ddy)ifnotposinvectors:vectors.add(pos)new_vectors_tmp.add(pos)new_vectors=new_vectors_tmpdefsetup_playerteam(self):foriinrange(1):x,y=self.find_free_hex(2,3)char=Character(self,self.hexmap)char.move_to_hex(x,y)char.team="trees"char.show()char.attack=18self.chars.append(char)defsetup_step(self,dt=0):"""setup the level."""# if we do not have characters yet, we add all of them.ifnotself.chars:self.setup_playerteam()# TODO: Make loading a character from a file actually work.#with open(join("data", "units", "rats.yml")) as f: #rat_data = f.read()# add ratsifself.num_rats>0:x,y=self.find_free_hex(7,1)char=Character(self,self.hexmap,image_path=join("graphics","wesnoth","giant-rat.png"),source="tag:1w6.org,2010:Rat")#, template_data = rat_data)char.move_to_hex(x,y)# we are the rats!char.team="rats"char.show()char.attack=6self.chars.append(char)self.num_rats-=1# and add goblinsifself.num_goblins>0:x,y=self.find_free_hex(6,-2)char=Character(self,self.hexmap,image_path=join("graphics","wesnoth","impaler.png"),source="tag:1w6.org,2010:Rat")#, template_data = rat_data)char.move_to_hex(x,y)# we are the rats!char.team="goblins"char.show()char.attack=9self.chars.append(char)self.num_goblins-=1# pass the control to the AIifself.num_goblins<=0andself.num_rats<=0:# sort by initiativeself.chars_by_initiative=[[c.roll_initiative(),c]forcinself.chars]self.chars_by_initiative.sort()self.phase["player"]="CPU"# if we’re not finished yet, stay in the setup step# TODO: Do this in steps be returning a result which only allows one action: next step.else:schedule_once(self.setup_step,0.02)defnext_by_initiative(self):"""Get the char with the highest initiative, reroll if needed."""# reroll while all are below 6. whileself.chars_by_initiative[-1][0]<6:# increase by at least 1, else this can go into deadlocks.self.chars_by_initiative=[[ini+max(1,c.roll_initiative()/3),c]forini,cinself.chars_by_initiative]self.chars_by_initiative.sort()# remove dead or inactive charsself.chars_by_initiative=[[ini,c]forini,cinself.chars_by_initiativeifc.activeandc.alive]# return the charreturnself.chars_by_initiative[-1][1]def_initiative_step_current(self,amount=12):self.chars_by_initiative[-1][0]-=amountself.chars_by_initiative.sort()defswitch_to_cpu_turn(self,dt=0):self.phase["player"]="CPU"defturn_finished(self):# unit acted ⇒ reduce initself._initiative_step_current()# let the computer actself.switch_to_cpu_turn()defcomputer_action(self,char):"""Let the selected char act once."""target=char.best_target_hex()char.move_to_hex(*target)char.attack_best_target_if_possible()self._initiative_step_current()defcomputer_turn(self):"""Let the computer act once."""# game end conditionteams=set([c.teamforcinself.hexmap.values()])# if someone won, nothing more to be done.iflen(teams)==1:data={"winner":list(teams)[0]}self.phase["player"]=NonereturnResult("finished",data)# if it’t the players turn, let him act.next_char=self.next_by_initiative()ifnext_char.team==self.player_team:# TODO: return the function to select an action for the char instead.self.phase["player"]="player"returnResult("player_turn",data=next_char,description="the player character who can act now")# otherwise battle on :)self.computer_action(next_char)self.phase["player"]="wait"# TODO: return the result of the action instead. Or at least a list of changed characters. Best a list of all characters who changed state and how they changed state.returnResult("step",commands=[Command(self.switch_to_cpu_turn,"continue")])classLogic:def__init__(self):self.model=Model()#: The scene to switch to at the next possible step.self.switch=Falsedefupdate(self):# Update the visible representation of the char.forcharinself.model.chars:char.update_model()# do the computer actions. As soon as it’s the players turn, computer_turn() changes the state.ifself.model.phase["player"]=="setup":returnResult("init",description="initialized the logic. Setup needed.",commands=[Command(self.model.setup_step,"setup")])elifself.model.phase["player"]=="CPU":returnResult("computer turn",commands=[Command(self.model.computer_turn,"computer turn")])