how the hell did I get this far without version control ohno1
This commit is contained in:
commit
25faf2d550
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
170
actions.py
Normal file
170
actions.py
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import random as ra
|
||||||
|
import json as j
|
||||||
|
from os.path import dirname,join
|
||||||
|
with open(join(dirname(__file__),'data.json')) as d:
|
||||||
|
data=j.load(d)
|
||||||
|
classtable=data['classes']
|
||||||
|
from . import decos
|
||||||
|
|
||||||
|
@decos.target()
|
||||||
|
@decos.guardcheck
|
||||||
|
def attack(self,other,*_):
|
||||||
|
admg,ddmg=_dealdmg(self,other)
|
||||||
|
return admg,ddmg,f"{self.name} dealt {admg} to {other.name}" + (f", received {ddmg} counter damage" if ddmg else "")
|
||||||
|
|
||||||
|
def allattack(self,*_):
|
||||||
|
# We'll start at this code and attack 6 units
|
||||||
|
start=(self.code//6+1)%2*6
|
||||||
|
units=self.board.getunits(*list(range(start,start+6)),codes=True)
|
||||||
|
ascore=dscore=0
|
||||||
|
for other in units:
|
||||||
|
# That would be an embarrassing bug.
|
||||||
|
if other is None: continue
|
||||||
|
if 'guard' in other.status:
|
||||||
|
other.status['guard']-=0.4
|
||||||
|
if other.status['guard']<=0: other.status.pop('guard')
|
||||||
|
admg,ddmg=_dealdmg(self,other)
|
||||||
|
ascore+=admg
|
||||||
|
dscore+=ddmg
|
||||||
|
return round(ascore*2/3),dscore,f"{self.name} dealt a total of {ascore} damage (earning {round(ascore*2/3)} points) to enemy units" + (f", and received a total of {dscore} damage in return" if dscore else "")
|
||||||
|
|
||||||
|
def _dealdmg(self,other,melee=None):
|
||||||
|
sinfo=classtable[self.cls]
|
||||||
|
oinfo=classtable[other.cls]
|
||||||
|
if melee is None: amelee='amelee' in sinfo['attrs']
|
||||||
|
else: amelee=melee
|
||||||
|
counter=amelee & ('counter' in oinfo['attrs'])
|
||||||
|
admg=_calcdmg(self,other,'magic' in sinfo['attrs'],self.cls!='musket')
|
||||||
|
if counter: ddmg=_calcdmg(other,self,'magic' in oinfo['attrs'],True)
|
||||||
|
else: ddmg=0
|
||||||
|
if not (type(admg)==bool and admg==False): # We didn't pop a shield, proceed with popping charge
|
||||||
|
if 'cancel' in sinfo['attrs']: other.status.pop('charging',None) # I'll need to mess with this later to make it handle super charge.
|
||||||
|
other.cur-=admg
|
||||||
|
self.cur-=ddmg
|
||||||
|
if other.cur<=0: o=other.die(); admg+=o[0]; ddmg+=o[1]
|
||||||
|
if self.cur<=0: s=self.die(); admg+=s[1]; ddmg+=s[0]
|
||||||
|
return admg,ddmg
|
||||||
|
|
||||||
|
def _calcdmg(self,other,magic=False,capped=True,counter=False):
|
||||||
|
# This only deals damage one direction. It's like a one-liner.
|
||||||
|
# Well but if it's magic damage, and protection status...
|
||||||
|
if 'protected' in other.status: other.status.pop('protected'); return 0
|
||||||
|
if not magic:
|
||||||
|
dmg=round((1.15**(self.atk-other.dfn)) \
|
||||||
|
*((self.cur*1000)**0.5)*0.1 \
|
||||||
|
*(0.6 if 'defend' in other.status else 1) \
|
||||||
|
* (0.5 if counter else 1))
|
||||||
|
else:
|
||||||
|
dmg=round((1.15**(self.atk*0.2+self.int-other.dfn*0.2-other.int)) \
|
||||||
|
*((self.cur*1000)**0.5)*0.1 \
|
||||||
|
*(0.6 if 'defend' in other.status else 1) \
|
||||||
|
* (0.5 if counter else 1))
|
||||||
|
if capped: return min(dmg,other.cur,self.cur)
|
||||||
|
else: return min(dmg,other.cur)
|
||||||
|
|
||||||
|
def guard(self,*_):
|
||||||
|
try: self.status['guard']+=self.int*0.2
|
||||||
|
except KeyError: self.status['guard']=self.int*0.2
|
||||||
|
self.status['defend']=True
|
||||||
|
return 0,0,f"{self.name} gains {self.int*0.2:.3} guard value"
|
||||||
|
|
||||||
|
@decos.target(ranged=True)
|
||||||
|
@decos.guardcheck
|
||||||
|
def assassinate(self,other,*_):
|
||||||
|
other.status.pop('protected',None)
|
||||||
|
chance=self.int*(self.cur/other.cur)*0.07
|
||||||
|
roll=ra.random()
|
||||||
|
if roll<chance:
|
||||||
|
o=other.die()
|
||||||
|
return 50+o[0],0+o[1],f"{self.name} assassinates {other.name} at {round(chance*100)}% chance!"
|
||||||
|
return 0,0,f"{self.name} fails to assassinate {other.name} at {round(chance*100)}% chance!"
|
||||||
|
|
||||||
|
@decos.target(ally=True)
|
||||||
|
def shield(self,other,*_):
|
||||||
|
# Actual real simple like
|
||||||
|
other.status['protected']=True
|
||||||
|
return 0,0,f"{self.name} shields {other.name}"
|
||||||
|
|
||||||
|
def guardcancel(self,*_):
|
||||||
|
start=(self.code//6+1)%2*6
|
||||||
|
for other in self.board.getunits(*list(range(start,start+6)),codes=True):
|
||||||
|
other.status.pop('guard',None); other.status.pop('defend',None)
|
||||||
|
return 0,0,f"{self.name} breaks all enemy guard"
|
||||||
|
|
||||||
|
@decos.target(ally=True)
|
||||||
|
def heal(self,other,*_):
|
||||||
|
healval=round(min(self.cur*0.03*self.int,other.max-other.cur))
|
||||||
|
other.cur+=healval
|
||||||
|
return 0,0,f"{self.name} heals {other.name} for {healval}"
|
||||||
|
|
||||||
|
def regenerate(self,*_):
|
||||||
|
heal=round(min(max(self.max*0.05,self.cur*0.2),self.max-self.cur))
|
||||||
|
self.cur+=heal
|
||||||
|
return 0,0,f"{self.name} heals itself for {heal}"
|
||||||
|
|
||||||
|
def ratingup(self,*_):
|
||||||
|
# Actual real final simple like ver1.0.1
|
||||||
|
# HAHAHAHAHA FORGOT TO MAKE THIS RETURN INTS INSTEAD OF FLOATS
|
||||||
|
return (round(75+15*self.int),0,f"{self.name} boosts score by {round(75+15*self.int)}")
|
||||||
|
|
||||||
|
@decos.target(ally=True)
|
||||||
|
def buff(self,other,*_):
|
||||||
|
buffnum=self.cur//100+1
|
||||||
|
targnum=buffnum//2 # This is actually +1 but ssh, we already have that one allocated.
|
||||||
|
start=self.code//6%2*6 # This actually evaluates correctly without the parens
|
||||||
|
targs=list(filter(lambda x:x.code!=other.code, self.board.getunits(*list(range(start,start+6)),codes=True)))
|
||||||
|
targs=ra.sample(targs,targnum)
|
||||||
|
targs.append(other)
|
||||||
|
out=[]
|
||||||
|
for n in targs:
|
||||||
|
out.extend([(n,m) for m in ['atk','def','int','spd'] if m not in n.status])
|
||||||
|
out2=[]
|
||||||
|
if len(out)<buffnum:
|
||||||
|
for n in targs: out2.extend([(n,m) for m in ['atk','def','int','spd'] if m in n.status])
|
||||||
|
out2=ra.sample(out2,buffnum-len(out))
|
||||||
|
out.extend(out2)
|
||||||
|
else: out=ra.sample(out,buffnum)
|
||||||
|
# We must always assume there are buffs that could get replaced, so we need to check if they should be.
|
||||||
|
for n in out:
|
||||||
|
# These are the stats to be buffed, paired with the unit on which they should be.
|
||||||
|
try: n[0].status[n[1]]=max(n[0].status[n[1]],0.1*self.int)
|
||||||
|
except KeyError: n[0].status[n[1]]=0.1*self.int
|
||||||
|
return 0,0,f"{self.name} buffs {', '.join([t.name for t in targs])}"
|
||||||
|
|
||||||
|
def debuff(self,*_):
|
||||||
|
targnum=self.cur//250+1
|
||||||
|
start=(self.code//6+1)%2*6 # This doesn't evaluate correctly without the parens; but that's all it needs.
|
||||||
|
targs=self.board.getunits(*list(range(start,start+6)),codes=True)
|
||||||
|
weights=[sum([True for n in ['atk','def','int','spd'] if n in i.status]) for i in targs]
|
||||||
|
# I'll probably move this out to utils later
|
||||||
|
# I have no idea what the above line is about. Maybe it's because it was so badly written. It's because of some straight bullshit. I will move it to utils now and do it properly.
|
||||||
|
# Didn't move it to utils but I did it properly. Fucking hell python, you're supposed to be good at this stuff.
|
||||||
|
out=_weighted_sample(targs,weights,targnum)
|
||||||
|
#out=[]
|
||||||
|
#for _ in range(targnum):
|
||||||
|
# out.append(weights.pop(ra.choices(list(range(len(weights))),weights)[0]))
|
||||||
|
# This should be our target list
|
||||||
|
for n in out: print(f'debuffing {n}'); n.status.pop('atk',None); n.status.pop('def',None); n.status.pop('int',None); n.status.pop('spd',None)
|
||||||
|
return 0,0,f"{self.name} clears buffs from {', '.join([t.name for t in out])}"
|
||||||
|
|
||||||
|
def _weighted_sample(items,weights,count=1,cum=False):
|
||||||
|
# Weights are not assumed to be cumulative by default.
|
||||||
|
if not len(items)==len(weights): raise ValueError('Length of weights should be the same as length of items.')
|
||||||
|
if cum:
|
||||||
|
for i in range(len(weights)):
|
||||||
|
weights[i]-=sum(weights[:i]) # Change from cumulative to arbitrary
|
||||||
|
out=[]
|
||||||
|
for _ in range(count):
|
||||||
|
pick=int(ra.random()*sum(weights))
|
||||||
|
i=0
|
||||||
|
while pick>weights[i]: pick-=weights[i];i+=1;
|
||||||
|
out.append(items[i])
|
||||||
|
items.pop(i); weights.pop(i) # Update list for next run
|
||||||
|
return out
|
||||||
|
|
||||||
|
def rest(self,*_):
|
||||||
|
# Real simple like
|
||||||
|
# Not so simple like
|
||||||
|
heal=round(max(self.max*0.015,min(self.cur*0.07,self.max-self.cur)))
|
||||||
|
self.cur+=heal
|
||||||
|
return 0,0,f"{self.name} rests and recovers {heal} troops"
|
77
automate.py
Normal file
77
automate.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import random as ra
|
||||||
|
import json as j
|
||||||
|
|
||||||
|
# Package imports
|
||||||
|
# Does Python have real package support?
|
||||||
|
from . import unit
|
||||||
|
from . import board
|
||||||
|
from os.path import dirname,join
|
||||||
|
with open(join(dirname(__file__),'data.json')) as d:
|
||||||
|
data=j.load(d)
|
||||||
|
board.actinfo=data['actions']
|
||||||
|
classlist=data['classes']
|
||||||
|
|
||||||
|
def randomunit(cls,name=None):
|
||||||
|
# TODO: Rewrite this to include a power-point system
|
||||||
|
if cls=='musket': return unit.RosterUnit(cls,*[wra(10,3,1) for _ in range(4)],wra(300,3,100),wra(3,1,1),name=name)
|
||||||
|
return unit.RosterUnit(cls,*[wra(10,3,1) for _ in range(4)],wra(600,3,300),wra(7,3,2),name=name)
|
||||||
|
|
||||||
|
def wra(top,dice,bot=0):
|
||||||
|
return bot+int(sum([ra.random()*(top-bot) for _ in range(dice)])/dice)
|
||||||
|
|
||||||
|
mcls=[k for k,v in classlist.items() if 'tmelee' in v['attrs']]
|
||||||
|
rcls=[k for k,v in classlist.items() if 'tranged' in v['attrs']]
|
||||||
|
def blindplayer(unitnum):
|
||||||
|
cls=yield {'melee':mcls,'ranged':rcls,'melees':'\n'.join(mcls),'rangeds':'\n'.join(rcls)}
|
||||||
|
if not (len(cls)==unitnum):
|
||||||
|
raise Exception("Picked the wrong unit count")
|
||||||
|
units=[randomunit(cl) for cl in cls]
|
||||||
|
# Ensure this gives back an integer
|
||||||
|
reroll=yield {'units':[u.static() for u in units],'strunits':'\n'.join([u.static() for u in units])}
|
||||||
|
new=randomunit(units[reroll].cls)
|
||||||
|
# A boolean
|
||||||
|
confirm=yield {'old':units[reroll].static(),'new':new.static()}
|
||||||
|
if confirm: units[reroll]=new
|
||||||
|
# List of integers (between 1 and 6)
|
||||||
|
names=yield {'strunits':'\n'.join([u.static() for u in units])}
|
||||||
|
for k,v in names.items(): units[k].name=v
|
||||||
|
positions=yield {'units':[u.static() for u in units],'strunits':'\n'.join([u.static() for u in units])}
|
||||||
|
units={positions[i]:units[i] for i in range(unitnum)}
|
||||||
|
yield {k:unit.BattleUnit.tobattle(u) for k,u in units.items()}
|
||||||
|
return
|
||||||
|
|
||||||
|
def blindstart(unitnum,p1,p2):
|
||||||
|
# Ensure this gives back a tuple of lists
|
||||||
|
# Haha yeah maybe listing the available classes is a good idea
|
||||||
|
# Haha yeah, maybe splitting them by range is a good idea
|
||||||
|
cls=yield mcls,rcls
|
||||||
|
if not (len(cls[0])==len(cls[1])==unitnum):
|
||||||
|
raise Exception("Someone picked the wrong unit count")
|
||||||
|
p1u=[randomunit(cl) for cl in cls[0]]
|
||||||
|
p2u=[randomunit(cl) for cl in cls[1]]
|
||||||
|
# Ensure this gives back a tuple of integers
|
||||||
|
reroll=yield [u.static() for u in p1u],[u.static() for u in p2u]
|
||||||
|
new=randomunit(p1u[reroll[0]].cls),randomunit(p2u[reroll[1]].cls)
|
||||||
|
# Tuple of booleans
|
||||||
|
confirm=yield tuple(u.static() for u in new)
|
||||||
|
if confirm[0]: p1u[reroll[0]]=new[0]
|
||||||
|
if confirm[1]: p2u[reroll[1]]=new[1]
|
||||||
|
# Tuple of lists of integers (between 1 and 6)
|
||||||
|
names=yield True
|
||||||
|
for k,v in names[0].items(): p1u[k].name=v
|
||||||
|
for k,v in names[1].items(): p2u[k].name=v
|
||||||
|
positions=yield [u.static() for u in p1u],[u.static() for u in p2u]
|
||||||
|
p1u={positions[0][i]:p1u[i] for i in range(unitnum)}
|
||||||
|
p2u={positions[1][i]:p2u[i] for i in range(unitnum)}
|
||||||
|
keys=yield True
|
||||||
|
p1u={k:unit.BattleUnit.tobattle(u) for k,u in p1u.items()}
|
||||||
|
p2u={k:unit.BattleUnit.tobattle(u) for k,u in p2u.items()}
|
||||||
|
return board.Board(p1,p2,keys[0],keys[1],p1u,p2u)
|
||||||
|
|
||||||
|
# Utility function for looking at damage scaling stuff. Not part of the game itself.
|
||||||
|
# I don't really remember how it works.
|
||||||
|
def bump(a):
|
||||||
|
for i in range(1,10):
|
||||||
|
for j in range(1,10):
|
||||||
|
print(f'{(a**(i-j)+1):.4f}',end=' ')
|
||||||
|
print()
|
169
board.py
Normal file
169
board.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import random as ra
|
||||||
|
|
||||||
|
from . import drawing
|
||||||
|
from . import decos
|
||||||
|
|
||||||
|
class Board():
|
||||||
|
def __init__(self,p1,p2,p1k,p2k,p1u,p2u):
|
||||||
|
self.grid=[None for _ in range(12)]
|
||||||
|
# p1u/p2u split from original units is because now they can both be in the form 0-5 and it JUST WORKS™
|
||||||
|
# Haha it had a bug anyway. P2's units were upside down.
|
||||||
|
for k,v in p1u.items(): self.grid[k]=v
|
||||||
|
for k,v in p2u.items(): self.grid[6+(3+k)%6]=v
|
||||||
|
self.p1={}; self.p2={}
|
||||||
|
self.p1['name']=p1; self.p2['name']=p2
|
||||||
|
self.p1['key']=p1k; self.p2['key']=p2k
|
||||||
|
self.p1['score']=0; self.p2['score']=0
|
||||||
|
for i in range(len(self.grid)):
|
||||||
|
if self.grid[i] is not None: self.grid[i].code=i
|
||||||
|
for i in self.grid:
|
||||||
|
if i is None: continue
|
||||||
|
# Just gonna sneak this into the name as well, until I support actual names.
|
||||||
|
if i.name=='': i.name=str(i.code)
|
||||||
|
i.board=self
|
||||||
|
if i.cls=='guardian': i.status['guard']=0.5
|
||||||
|
if i.cls=='tactician':
|
||||||
|
start=i.code//6%2*6
|
||||||
|
targs=[n for n in self.getunits(*list(range(start,start+6)),codes=True)]
|
||||||
|
bufftargs=[]
|
||||||
|
for n in targs:
|
||||||
|
bufftargs.extend([(n,m) for m in ['atk','def','int','spd'] if m not in n.status])
|
||||||
|
bufftargs=ra.sample(bufftargs,3)
|
||||||
|
for n in bufftargs: n[0].status[n[1]]=0.1*i.int
|
||||||
|
i.speedval=i.aspd(70)+ra.random()*0.0001
|
||||||
|
self.turn=0
|
||||||
|
|
||||||
|
def controller(self):
|
||||||
|
# The main game loop thing.
|
||||||
|
# The two yield statements do serve a purpose.
|
||||||
|
# Sometimes we don't want anything back in (when a charged action is to occur)
|
||||||
|
# So we send a code that says "don't send us anything next step" along with the result
|
||||||
|
# Setup stuff goes here? Maybe?
|
||||||
|
while self.uptobat():
|
||||||
|
try: u=self.getactive()
|
||||||
|
except AttributeError: break
|
||||||
|
try:
|
||||||
|
if 'charging' in u.status:
|
||||||
|
action=yield None
|
||||||
|
result=self.act2(u,*u.status['action'])
|
||||||
|
u.status.pop('action',None)
|
||||||
|
u.status.pop('charging',None)
|
||||||
|
else:
|
||||||
|
actviews=[]
|
||||||
|
lerst=self.bs()
|
||||||
|
for action in u.actions:
|
||||||
|
actd=actinfo[action]
|
||||||
|
spd=u.aspd(actd.get('startup',actd['recovery']))
|
||||||
|
after=self.bs(spd)
|
||||||
|
actviews.append((action,f"flags: {actd['flags']}",f"re-q: {after} (after {lerst[after-1].name})"))
|
||||||
|
action=yield ((self.p1['name'],self.p1['key']) if u.code<6 else (self.p2['name'],self.p2['key']), u.name, actviews)
|
||||||
|
u.status.pop('action',None) # If he got cancelled while charging, clean up
|
||||||
|
result=self.act1(u,*action)
|
||||||
|
except decos.RanceException as ex: result=str(ex)
|
||||||
|
yield result
|
||||||
|
return f"Good game, {self.p1['name'] if self.p1['score']>self.p2['score'] else self.p2['name'] if self.p1['score']<self.p2['score'] else 'nobody'} wins."
|
||||||
|
|
||||||
|
def uptobat(self):
|
||||||
|
self.p1['dead']=len(self.getunits(*list(range(0,6)),codes=True,dead=False))==0
|
||||||
|
self.p2['dead']=len(self.getunits(*list(range(6,12)),codes=True,dead=False))==0
|
||||||
|
if self.p1['dead']: self.p1['score']=-1
|
||||||
|
if self.p2['dead']: self.p2['score']=-1
|
||||||
|
if self.p1['dead'] or self.p2['dead']: return False # gg, a player died
|
||||||
|
if not self.getunits(*list(range(3,6)),codes=True,dead=False):
|
||||||
|
self.twizzle(True)
|
||||||
|
if not self.getunits(*list(range(6,9)),codes=True,dead=False):
|
||||||
|
self.twizzle(False)
|
||||||
|
try: tiny=self.bs()[0].speedval
|
||||||
|
except IndexError: return False # There's nobody left to act, just give up.
|
||||||
|
if tiny==0: return True # We're already where we should be, don't increment turn counter or w/e.
|
||||||
|
for k in self.grid:
|
||||||
|
if k is None: continue
|
||||||
|
k.speedval-=tiny
|
||||||
|
self.turn+=1
|
||||||
|
if self.turn>30: return False # Game's over.
|
||||||
|
return True
|
||||||
|
|
||||||
|
def twizzle(self,leftside):
|
||||||
|
# This is very broken, needs to fix turn order stuff.
|
||||||
|
# It's less broken now but it's still kinda bad.
|
||||||
|
if leftside:
|
||||||
|
temp=self.grid[0:3]
|
||||||
|
self.grid[0:3]=self.grid[3:6]
|
||||||
|
self.grid[3:6]=temp
|
||||||
|
else:
|
||||||
|
temp=self.grid[9:12]
|
||||||
|
self.grid[9:12]=self.grid[6:9]
|
||||||
|
self.grid[6:9]=temp
|
||||||
|
for i in range(12):
|
||||||
|
try: self.grid[i].code=i
|
||||||
|
except AttributeError: continue
|
||||||
|
|
||||||
|
def act1(self,u,action,target=None,*_):
|
||||||
|
actd=actinfo[action]
|
||||||
|
t=self.getunits(target,action=action)
|
||||||
|
if t is not None: t=t[0]
|
||||||
|
if u.curflags<actd['flags'] or u.curflags<=0: return f'Unit {u.name} too exhausted for {action}'
|
||||||
|
if 'startup' in actd:
|
||||||
|
# Charged action, don't actually do it, just fuck off to some other routine.
|
||||||
|
u.status['charging']=True
|
||||||
|
u.status['action']=(action,target)
|
||||||
|
u.speedval+=u.aspd(actd['startup'])
|
||||||
|
return f"{u.name} begins charging"
|
||||||
|
return self.act2(u,action,t)
|
||||||
|
|
||||||
|
def act2(self,u,action,t,*_):
|
||||||
|
actd=actinfo[action]
|
||||||
|
# The important line
|
||||||
|
# Also it's not a class/object method lol, so need to explicitly pass self
|
||||||
|
try: scores=u.actions[action](u,t)
|
||||||
|
except IndexError: raise decos.RanceException(f'Unit {u.name} doesn\'t have {action} action')
|
||||||
|
if u.code<6: self.p1['score']+=scores[0];self.p2['score']+=scores[1]
|
||||||
|
else: self.p2['score']+=scores[0];self.p1['score']+=scores[1]
|
||||||
|
if not (action=='guard' or action=='rest'):
|
||||||
|
u.status.pop('guard',None); u.status.pop('defend',None)
|
||||||
|
# I should probably be setting this instead of adding it.
|
||||||
|
u.speedval+=u.aspd(actd['recovery'])
|
||||||
|
# No action will ever cost more than 10 flags. And, more importantly, no unit will ever have more than 10 flags.
|
||||||
|
u.curflags-=min(actd['flags'] if actd['flags']>=0 else 10,u.curflags)
|
||||||
|
#if u.curflags==0: self.order.pop(u.code)
|
||||||
|
return scores[2]
|
||||||
|
|
||||||
|
def getunits(self,*units,action=None,codes=False,dead=False):
|
||||||
|
# The mega get function. No grid access should occur without this being called.
|
||||||
|
if action:
|
||||||
|
# We're in targeting mode.
|
||||||
|
if 'target' not in actinfo[action]['attrs']: return None
|
||||||
|
if len(units)!=1 or units[0] is None: raise decos.RanceException(f'You must target one unit with {action}')
|
||||||
|
out=self.resolvenames(*units,codes=codes,dead=dead)
|
||||||
|
if not dead: out=[u for u in out if 'dead' not in u.status] # Not sure why this line is here, if dead is False it should be correctly filtered out in resolvenames.
|
||||||
|
if action and not out: raise decos.RanceException(f'Invalid target for {action}')
|
||||||
|
return out
|
||||||
|
|
||||||
|
def resolvenames(self,*units,codes=False,dead=False):
|
||||||
|
if codes: return [u for u in self.grid if u is not None and u.code in units and ((not dead) or 'dead' not in u.status)]
|
||||||
|
names={u:u.name for u in self.grid if u is not None}
|
||||||
|
out=[]
|
||||||
|
for unit in units:
|
||||||
|
res=[k for k,v in names.items() if v==unit]
|
||||||
|
if len(res)!=1:
|
||||||
|
try: o=self.grid[int(unit)]
|
||||||
|
except ValueError: raise decos.RanceException(f'Name {unit} is ambiguous; matches {len(res)} units')
|
||||||
|
if o is not None: out.append(o)
|
||||||
|
else: out.append(res[0])
|
||||||
|
return out
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return drawing.drawboard(self)
|
||||||
|
|
||||||
|
def getactive(self):
|
||||||
|
try: return self.bs()[0]
|
||||||
|
except IndexError: return None
|
||||||
|
|
||||||
|
def bs(self,sim=None):
|
||||||
|
out=[x for x in self.grid if x is not None and x.curflags>0]
|
||||||
|
if sim is not None:
|
||||||
|
out2={x.code:x.speedval for x in out}
|
||||||
|
out2[-1]=sim
|
||||||
|
ind=sorted(out2.items(),key=lambda x:x[1]).index((-1,sim))
|
||||||
|
return ind
|
||||||
|
return sorted(out,key=lambda x:x.speedval)
|
21
classdata.py
Normal file
21
classdata.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Class():
|
||||||
|
name:str
|
||||||
|
satk:float
|
||||||
|
sdef:float
|
||||||
|
sint:float
|
||||||
|
sspd:float
|
||||||
|
code:str=''
|
||||||
|
emoji:str=''
|
||||||
|
attrs:list=[]
|
||||||
|
inherent:list=[]
|
||||||
|
default_skills:list=[]
|
||||||
|
allowed_skills:list=[]
|
||||||
|
|
||||||
|
guardian=Class('Guardian', 0.95,1.1,1.0,1.0, 'gd','🛡', ['amelee','tmelee','counter'], [skill.initialguard], [skill.ally_guard, skill.attack], [skill.ally_guard_plus, skill.all_guard, skill.counterattack2, skill.counterattack3])
|
||||||
|
|
||||||
|
warrior=Class('Warrior', 1.3,1.0,0.9,1.1, 'wa','🗡', ['amelee','tmelee','counter'], [], [skill.attack], [skill.attack2, skill.charge, skill.light_attack])
|
||||||
|
|
||||||
|
archer=Class('Archer', 1.2,1.0,1.0,0.95, 'ar','🏹', ['aranged','tranged'], [], [skill.attack],
|
28
data.json
Normal file
28
data.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"classes":{
|
||||||
|
"guardian":{"atk":0.95,"def":1.1,"int":1.0,"spd":1.0,"icon":"🛡","icon2":"df","attrs":["amelee","tmelee","counter"],"actions":["attack","guard"]},
|
||||||
|
"warrior":{"atk":1.3,"def":1.0,"int":0.9,"spd":1.1,"icon":"🗡","icon2":"wa","attrs":["amelee","tmelee","counter"],"actions":["attack"]},
|
||||||
|
"archer":{"atk":1.2,"def":1.0,"int":1.0,"spd":0.95,"icon":"🏹","icon2":"ar","attrs":["aranged","tranged"],"actions":["attack"]},
|
||||||
|
"miko":{"atk":0.95,"def":0.95,"int":1.0,"spd":1.0,"icon":"⚕","icon2":"mi","attrs":["aranged","tranged"],"actions":["attack","heal"]},
|
||||||
|
"ninja":{"atk":0.97,"def":1.03,"int":0.95,"spd":1.3,"icon":"👤","icon2":"ni","attrs":["aranged","tranged","counter","cancel"],"actions":["attack","assassinate"]},
|
||||||
|
"diviner":{"atk":0.75,"def":0.85,"int":0.8,"spd":0.9,"icon":"☯️🧙","icon2":"dv","attrs":["aranged","tranged","magic"],"actions":["allattack","shield"]},
|
||||||
|
"tactician":{"atk":1.0,"def":1.0,"int":1.0,"spd":1.0,"icon":"🧠","icon2":"tc","attrs":["aranged","tranged"],"actions":["attack","buff","debuff","ratingup"]},
|
||||||
|
"monk":{"atk":1.0,"def":1.0,"int":1.1,"spd":1.0,"icon":"📿","icon2":"mo","attrs":["amelee","tmelee","counter"],"actions":["attack","guardcancel","regenerate"]},
|
||||||
|
"musket":{"atk":2.5,"def":1.0,"int":1.0,"spd":1.1,"icon":"🔫","icon2":"ms","attrs":["tmelee","aranged"],"actions":["attack"]}},
|
||||||
|
|
||||||
|
"actions":{
|
||||||
|
"attack":{"flags":1,"recovery":50,"attrs":["target"]},
|
||||||
|
"allattack":{"flags":1,"startup":50,"recovery":50,"attrs":[]},
|
||||||
|
"guard":{"flags":1,"recovery":35,"attrs":[]},
|
||||||
|
"assassinate":{"flags":-1,"recovery":-1,"attrs":["target"]},
|
||||||
|
"shield":{"flags":1,"recovery":40,"attrs":["target"]},
|
||||||
|
"heal":{"flags":1,"recovery":50,"attrs":["target"]},
|
||||||
|
"ratingup":{"flags":2,"recovery":30,"attrs":[]},
|
||||||
|
"buff":{"flags":1,"recovery":55,"attrs":["target"]},
|
||||||
|
"debuff":{"flags":2,"recovery":45,"attrs":[]},
|
||||||
|
"guardcancel":{"flags":1,"recovery":40,"attrs":[]},
|
||||||
|
"regenerate":{"flags":2,"recovery":50,"attrs":[]},
|
||||||
|
"rest":{"flags":0,"recovery":80,"attrs":[]}},
|
||||||
|
|
||||||
|
"statii":["atk","def","int","spd","","protected","","charging","guard"]
|
||||||
|
}
|
54
decos.py
Normal file
54
decos.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
#DECO BEEEEEEEEEENCH
|
||||||
|
|
||||||
|
# We in it bois
|
||||||
|
import functools
|
||||||
|
import random as ra
|
||||||
|
import json as j
|
||||||
|
from os.path import dirname,join
|
||||||
|
with open(join(dirname(__file__),'data.json')) as d:
|
||||||
|
data=j.load(d)
|
||||||
|
classlist=data['classes']
|
||||||
|
|
||||||
|
# This is useful
|
||||||
|
class RanceException(Exception):
|
||||||
|
# This doesn't actually need any particular content. I just need the type to exist.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def target(ranged=None,ally=False):
|
||||||
|
def owrap(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrap(self,other,*args,**kwargs):
|
||||||
|
if not other: raise RanceException(f"You must target a unit with {func.__name__}")
|
||||||
|
if ranged==None:
|
||||||
|
if ally: rangeda=True # If it's an ally-targeting ability, assume it's ranged. It'd be really weird to have a melee-range ally-targeting ability.
|
||||||
|
else: rangeda='tranged' in classlist[self.cls]['attrs'] # We have to figure out if this unit is ranged or melee
|
||||||
|
else: rangeda=ranged # Assigning a local variable at any point in the function causes that name to refer to it at all points in the function.
|
||||||
|
# The line above this was throwing a "can't read unassigned variable" error because of this, until I just changed the name internally.
|
||||||
|
if not rangeda and not (3<=self.code<=8 and 3<=other.code<=8): raise RanceException(f"{self.name} can't reach {other.name} for melee {func.__name__}")
|
||||||
|
if not (self.code<6)^(other.code<6)^ally:
|
||||||
|
raise RanceException(f"Unit {self.name} can't {func.__name__} {'enemy' if ally else 'allied'} unit {other.name}")
|
||||||
|
return func(self,other,*args,**kwargs)
|
||||||
|
return wrap
|
||||||
|
return owrap
|
||||||
|
|
||||||
|
# This will return a function now.
|
||||||
|
def guardcheck(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrap(self,other,*args,**kwargs):
|
||||||
|
strong=[]
|
||||||
|
guards=self.board.getunits(*list(range(other.code//3*3,other.code//3*3+3)),codes=True)
|
||||||
|
for i in guards:
|
||||||
|
if i.code==other.code or 'guard' not in i.status: continue
|
||||||
|
if ra.random()<i.status['guard']:
|
||||||
|
other=i
|
||||||
|
strong.append(f"{i.name} guards the attack")
|
||||||
|
break
|
||||||
|
else: strong.append(f"{i.name} fails to guard")
|
||||||
|
if 'guard' in other.status:
|
||||||
|
other.status['guard']-=0.4
|
||||||
|
if other.status['guard']<=0.001:
|
||||||
|
other.status.pop('guard')
|
||||||
|
out=', '.join(strong)+'. ' if strong else ''
|
||||||
|
a,b,c=func(self,other,*args,**kwargs)
|
||||||
|
return a,b,out+c
|
||||||
|
return wrap
|
118
drawing.py
Normal file
118
drawing.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import json as j
|
||||||
|
from os.path import dirname,join
|
||||||
|
with open(join(dirname(__file__),'data.json')) as d:
|
||||||
|
data=j.load(d)
|
||||||
|
classtable=data['classes']
|
||||||
|
|
||||||
|
boxes='─│┌┐└┘├┤┬┴┼'
|
||||||
|
|
||||||
|
# Generic table creation function. Not used.
|
||||||
|
def createtable(data):
|
||||||
|
#Get the column widths
|
||||||
|
colnum=len(data[0])
|
||||||
|
#Gotta init this so I can iterate backwards
|
||||||
|
colwidths=[0 for _ in range(colnum)]
|
||||||
|
for row in data:
|
||||||
|
i=colnum
|
||||||
|
while i>0:
|
||||||
|
#Can't forget this now.
|
||||||
|
i-=1
|
||||||
|
colwidths[i]=max(colwidths[i],len(str(row[i])))
|
||||||
|
#Create header row
|
||||||
|
table='┌{0}┐'.format('┬'.join(['─'*width for width in colwidths]))
|
||||||
|
#Assume data[0] is the title row
|
||||||
|
i=-1
|
||||||
|
rowbuild=''
|
||||||
|
while i<(colnum-1):
|
||||||
|
i+=1
|
||||||
|
rowbuild+='│{0}{1}'.format(data[0][i],' '*(colwidths[i]-len(str(data[0][i]))))
|
||||||
|
table+='\n│{0}│'.format(rowbuild[1:])
|
||||||
|
table+='\n├{0}┤'.format('┼'.join(['─'*width for width in colwidths]))
|
||||||
|
#Now the final loop to do the rest of the table, then all I need is the bottom row.
|
||||||
|
for row in data[1:]:
|
||||||
|
i=-1
|
||||||
|
rowbuild=''
|
||||||
|
while i<(colnum-1):
|
||||||
|
i+=1
|
||||||
|
rowbuild+='│{0}{1}'.format(row[i],' '*(colwidths[i]-len(str(row[i]))))
|
||||||
|
table+='\n│{0}│'.format(rowbuild[1:])
|
||||||
|
table+='\n└{0}┘'.format('┴'.join(['─'*width for width in colwidths]))
|
||||||
|
return table
|
||||||
|
|
||||||
|
def drawboard(self,unitsize=15):
|
||||||
|
# This might become a __str__ alias later.
|
||||||
|
|
||||||
|
# How much width each unit is given in the board.
|
||||||
|
#unitsize=15
|
||||||
|
# How much width the whole board is (generally 4 units+7)
|
||||||
|
boardsize=unitsize*4+7
|
||||||
|
|
||||||
|
# Header row, includes player names and scores.
|
||||||
|
n1,s1,n2,s2=self.p1['name'],str(self.p1['score']),self.p2['name'],str(self.p2['score'])
|
||||||
|
row1a=f'┌─{n1[:30-len(s1)]}─{s1}'
|
||||||
|
row1b=f'{s2}─{n2[:30-len(s2)]}─┐'
|
||||||
|
row1m='─'*(boardsize-len(row1a)-len(row1b))
|
||||||
|
row1=row1a+row1m+row1b
|
||||||
|
|
||||||
|
# Turn order row. Primarily a list of unit names. Please fill out unit names.
|
||||||
|
row2=', '.join(map(lambda x:x.name,self.bs()))
|
||||||
|
if len(row2)>boardsize-4:
|
||||||
|
row2=row2[:boardsize-7]+'...'
|
||||||
|
lon=boardsize-4-len(row2)
|
||||||
|
if lon>0:
|
||||||
|
row2+=' '*lon
|
||||||
|
row2=f'│ {row2} │'
|
||||||
|
|
||||||
|
# Spacer row. Static.
|
||||||
|
row3='├'+'─'*unitsize+'┬'+'─'*unitsize+'┬─┬'+'─'*unitsize+'┬'+'─'*unitsize+'┤'
|
||||||
|
|
||||||
|
# First row of unit data; will do this in blocks of three. Somehow.
|
||||||
|
# Units 0, 3, 6 and 9. And "T u r".
|
||||||
|
# Haha fuck the above I'm just doing all the chunks and then stitching them together last.
|
||||||
|
# Haha fuck *that* above, I'm simplifying by inserting the chunks where they belong instantly.
|
||||||
|
rowwho=[[],[],[],[],[],[],[],[],[]]
|
||||||
|
i=0
|
||||||
|
for unit in self.grid:
|
||||||
|
if unit==None:
|
||||||
|
rowwho[(i%3)*3].append(' '*unitsize)
|
||||||
|
rowwho[(i%3)*3+1].append(' '*unitsize)
|
||||||
|
rowwho[(i%3)*3+2].append(' '*unitsize)
|
||||||
|
i+=1
|
||||||
|
continue
|
||||||
|
# Name and flags
|
||||||
|
nem2=f'{unit.curflags}/{unit.flags} ' # Hopefully this is never above 9 (3)
|
||||||
|
try:
|
||||||
|
if i==self.bs()[0].code:
|
||||||
|
nem1=' '+unit.name[:unitsize-5-len(nem2)]+'(*)'
|
||||||
|
else:
|
||||||
|
nem1=' '+unit.name[:unitsize-2-len(nem2)]
|
||||||
|
except IndexError:
|
||||||
|
nem1=' '+unit.name[:unitsize-2-len(nem2)]
|
||||||
|
nem=nem1+' '*(unitsize-len(nem1)-len(nem2))+nem2
|
||||||
|
rowwho[(i%3)*3].append(nem)
|
||||||
|
# Class and size
|
||||||
|
cl=classtable[unit.cls]['icon2']
|
||||||
|
siz=f'{unit.cur}/{unit.max}'
|
||||||
|
rowwho[(i%3)*3+1].append(' '+cl+' '*(unitsize-4-len(siz))+siz+' ')
|
||||||
|
# Status
|
||||||
|
rowwho[(i%3)*3+2].append(' '+unit.multistatus()[:unitsize-2]+' '*(unitsize-1-len(unit.multistatus()[:unitsize-2])))
|
||||||
|
# If we're at the current active unit's position
|
||||||
|
try:
|
||||||
|
if i==self.bs()[0].code:
|
||||||
|
rowwho[(i%3)*3][-1]=rowwho[(i%3)*3][-1].replace(' ','>',1)
|
||||||
|
rowwho[(i%3)*3+1][-1]=rowwho[(i%3)*3+1][-1].replace(' ','>',1)
|
||||||
|
rowwho[(i%3)*3+2][-1]=rowwho[(i%3)*3+2][-1].replace(' ','>',1)
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
i+=1
|
||||||
|
turner=list(f'Turn {self.turn:02} ')
|
||||||
|
rowsir=[]
|
||||||
|
for n in rowwho:
|
||||||
|
# Pre-prepared turn counter goes in
|
||||||
|
n.insert(2,turner.pop(0))
|
||||||
|
rowsir.append('│'+'│'.join(n)+'│')
|
||||||
|
rowsir='\n'.join(rowsir)
|
||||||
|
# Spacer row. Static.
|
||||||
|
row7='└'+'─'*unitsize+'┴'+'─'*unitsize+'┴─┴'+'─'*unitsize+'┴'+'─'*unitsize+'┘'
|
||||||
|
# Hope it looks right
|
||||||
|
return row1+'\n'+row2+'\n'+row3+'\n'+rowsir+'\n'+row7
|
206
games.py
Normal file
206
games.py
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
from objects.rance import automate
|
||||||
|
import random as ra
|
||||||
|
import asyncio
|
||||||
|
import discord
|
||||||
|
|
||||||
|
excluded=False
|
||||||
|
commands={'rance':[], 'rancequit':[], 'rancehack':[], 'rancerank':[], 'azul':[]}
|
||||||
|
|
||||||
|
rancegames={}
|
||||||
|
|
||||||
|
async def rance(args, channel, message):
|
||||||
|
if channel.id in rancegames and not type(rancegames[channel.id])==tuple:
|
||||||
|
await channel.send("Game already in progress, just watch or pick another channel")
|
||||||
|
return
|
||||||
|
p1=message.author
|
||||||
|
if len(args)==1:
|
||||||
|
try:
|
||||||
|
num=int(args[0])
|
||||||
|
except ValueError: num=None
|
||||||
|
if num is not None and 0<num<7: rancegames[channel.id]=(p1,num); await channel.send("Game is open! Awaiting other party!")
|
||||||
|
return
|
||||||
|
elif len(args)==0 and type(rancegames[channel.id])==tuple:
|
||||||
|
p1=rancegames[channel.id][0]; p2=message.author; num=rancegames[channel.id][1]
|
||||||
|
else:
|
||||||
|
p2=u.findobj(args[0],channel.guild.members)
|
||||||
|
if p2 is None:
|
||||||
|
await channel.send("Challenging players directly doesn't work anymore, thanks to intents.")
|
||||||
|
#await channel.send("First argument should name another player (mention, username, or id)")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
num=int(args[1])
|
||||||
|
except ValueError:
|
||||||
|
await channel.send("Second argument should be number of units (1-6)")
|
||||||
|
return
|
||||||
|
if not 1<=num<=6:
|
||||||
|
await channel.send("Number of units should be between 1 and 6, inclusive")
|
||||||
|
return
|
||||||
|
await rancesetup(p1,p2,channel,num)
|
||||||
|
await playrance(channel)
|
||||||
|
|
||||||
|
async def rancerank(args,channel,message):
|
||||||
|
if channel.id in rancegames: await channel.send("Game already in progress, just watch or pick another channel"); return
|
||||||
|
if len(args)==0: await channel.send("You need to challenge an opponent with this."); return
|
||||||
|
p2=u.findobj(' '.join(args),channel.guild.members)
|
||||||
|
if p2 is None: await channel.send("Couldn't find opponent; try mentioning them."); return
|
||||||
|
p1=message.author
|
||||||
|
rancegames[channel.id]=True
|
||||||
|
def validateunits(x):
|
||||||
|
n=x.split('\n')
|
||||||
|
for m in n[1:]:
|
||||||
|
if len(m.split(','))!=9: return f"line {m} is invalid somehow, please redo it"
|
||||||
|
return True
|
||||||
|
units=await getnp("Please enter your name, then the units you wish to bring to battle, in the following format:\npos,class,atk,def,int,spd,troops,flags,name\nPlease enter one unit per line, but the first line should be your name.\nAlso note that this will be logged; this function is intended for tournament play, where you have an existing roster. If cheating is suspected, the logs will be reviewed. Try not to use it frivolously during tournaments.",[p1,p2],[[],[]],[validateunits])
|
||||||
|
u.log('',*units) # That just makes the logs look a bit neater.
|
||||||
|
units=units[0].split('\n'),units[1].split('\n')
|
||||||
|
names=units[0][0],units[1][0]
|
||||||
|
units=[n.split(',') for n in units[0][1:]],[n.split(',') for n in units[1][1:]]
|
||||||
|
rancegames[channel.id]=board=automate.Board(names[0],names[1],p1.id,p2.id,{int(m[0])-1:automate.unit.BattleUnit(m[1],*[int(n) for n in m[2:-1]],name=m[-1]) for m in units[0]},{int(m[0])-1:automate.unit.BattleUnit(m[1],*[int(n) for n in m[2:-1]],name=m[-1]) for m in units[1]})
|
||||||
|
await p1.send(f"Game's ready in <#{channel.id}>")
|
||||||
|
await p2.send(f"Game's ready in <#{channel.id}>")
|
||||||
|
await playrance(channel)
|
||||||
|
|
||||||
|
async def rancesetup(p1,p2,channel,num):
|
||||||
|
# All checks finally out of the way zzzz
|
||||||
|
rancegames[channel.id]=seed=ra.random()
|
||||||
|
await channel.send("ゲーム・スタート!")
|
||||||
|
# Fire off the first set of listeners; the player names.
|
||||||
|
try:
|
||||||
|
names=await getnp("Please enter player name.",[p1,p2],[[],[]],[])
|
||||||
|
except (discord.Forbidden,discord.HTTPException) as ex:
|
||||||
|
await channel.send("Couldn't DM one of the players (can't tell who yet), maybe their DM settings are wrong; perhaps try DMing me first.")
|
||||||
|
rancegames.pop(channel.id)
|
||||||
|
return
|
||||||
|
if not rancestatus(channel.id,seed): return
|
||||||
|
berd=automate.blindstart(num,*names)
|
||||||
|
classlist=next(berd)
|
||||||
|
# https://stackoverflow.com/a/44780467
|
||||||
|
nl='\n'
|
||||||
|
longth=lambda x:len(x.split(' '))==num or "Wrong number of units provided"
|
||||||
|
def incls(x):
|
||||||
|
out=[]
|
||||||
|
for n in x.split(' '):
|
||||||
|
if n not in automate.classlist: out.append(n)
|
||||||
|
if out: return f"Invalid class names: {', '.join(out)}. Please note they're case sensitive."
|
||||||
|
return True
|
||||||
|
classes=await getnp(f"What classes would you like for your **{num}** units? (Space separated)\nList of melee classes (work best in front row):\n{nl.join(classlist[0])}\n\nRanged (work best in back row):\n{nl.join(classlist[1])}",[p1,p2],[[],[]],[longth,incls])
|
||||||
|
if not rancestatus(channel.id,seed): return
|
||||||
|
units=berd.send(tuple([m.lower() for m in n.split(' ')] for n in classes))
|
||||||
|
def isint(x):
|
||||||
|
try: int(x); return True
|
||||||
|
except ValueError: return f"'{x}' isn't a valid integer"
|
||||||
|
rerolls=await getnp("Your units:\n{0}\n Please pick a number (from 1 to {1}) to reroll; you can discard the reroll, don't worry",[p1,p2],[['\n'.join(units[0]),num],['\n'.join(units[1]),num]],[isint,lambda x:int(x) in range(1,num+1) or f"{x} isn't a valid unit number"])
|
||||||
|
rerolls=tuple(int(n)-1 for n in rerolls)
|
||||||
|
if not rancestatus(channel.id,seed): return
|
||||||
|
news=berd.send(rerolls)
|
||||||
|
boolaliases={'new':True,'old':False, 'yes':True,'no':False, 'true':True,'false':False, '1':True,'0':False, 'y':True,'n':False}
|
||||||
|
confirms=await getnp("Old unit: {0}\nNew unit: {1}\nWhich would you like to keep (old/new)?",[p1,p2],[[units[0][rerolls[0]],news[0]],[units[1][rerolls[1]],news[1]]],[lambda x:x in boolaliases or "Please specify either `old` or `new`"])
|
||||||
|
if not rancestatus(channel.id,seed): return
|
||||||
|
_=berd.send(tuple(boolaliases[n.lower()] for n in confirms))
|
||||||
|
def checknames(x):
|
||||||
|
if x in ('-','`-`'): return True
|
||||||
|
try: a={int(n.split(':')[0]):n.split(':')[1] for n in x.split(' ')}
|
||||||
|
except: return 'The format was weird in some way, please try again.'
|
||||||
|
if min(list(a))<1 or max(list(a))>num+1: return f"Numbers should be between 1 and {num} inclusive."
|
||||||
|
return True
|
||||||
|
unames=await getnp('You may provide names for your units, if you wish. Please provide them as num:name pairs, eg. `2:Ran 3:Rin`\nUnnamed units will be assigned a number as their name. Say `-` if you don\'t want to name any units.',[p1,p2],[[],[]],[checknames])
|
||||||
|
if not rancestatus(channel.id,seed): return
|
||||||
|
def isints(x):
|
||||||
|
for n in x.split(' '):
|
||||||
|
try: int(n)
|
||||||
|
except ValueError: return f"'{n}' isn't a valid integer"
|
||||||
|
if int(n) not in range(1,7): return f"{n} isn't a valid position"
|
||||||
|
return True
|
||||||
|
units=berd.send(({int(n.split(':')[0])-1:n.split(':')[1] for n in unames[0].split(' ')} if unames[0]!='-' else {},{int(n.split(':')[0])-1:n.split(':')[1] for n in unames[1].split(' ')} if unames[1]!='-' else {}))
|
||||||
|
positions=await getnp("Final unit list:\n{0}\nPlease enter the positions for these units (1-3 back row (ranged units should go here), 4-6 front row (melee units should go here), space separated, positions should be unique!)",[p1,p2],[['\n'.join(units[0])],['\n'.join(units[1])]],[lambda x:len(x.split(' '))==num or f"You should provide {num} position numbers", isints, lambda x:len(set(x.split(' ')))==len(x.split(' ')) or "Position numbers must be unique"])
|
||||||
|
if not rancestatus(channel.id,seed): return
|
||||||
|
_=berd.send(([int(n)-1 for n in positions[0].split(' ')],[int(n)-1 for n in positions[1].split(' ')]))
|
||||||
|
try: berd.send((p1.id,p2.id))
|
||||||
|
except StopIteration as ex:
|
||||||
|
board=ex.value
|
||||||
|
# We have a board!
|
||||||
|
rancegames[channel.id]=board
|
||||||
|
await p1.send(f"Game's ready in <#{channel.id}>")
|
||||||
|
await p2.send(f"Game's ready in <#{channel.id}>")
|
||||||
|
|
||||||
|
async def playrance(channel,board=None):
|
||||||
|
nl='\n'
|
||||||
|
if board is None: board=rancegames[channel.id]
|
||||||
|
controller=board.controller()
|
||||||
|
await channel.send("If a unit wasn't named, its unit code became its name.\nファイト!")
|
||||||
|
# No auto quit yet.
|
||||||
|
while True:
|
||||||
|
if not rancestatus(channel.id,board): return
|
||||||
|
# We can discard this during charged actions but if we don't get it shit goes wrong.
|
||||||
|
try: active=next(controller)
|
||||||
|
except StopIteration as ex: end=ex; break
|
||||||
|
if active is None:
|
||||||
|
# Charged action is happening. Don't wait for action or anything.
|
||||||
|
action=None
|
||||||
|
else:
|
||||||
|
await channel.send(f'```{board}```')
|
||||||
|
await channel.send(f"Available actions for {active[0][0]}'s unit {active[1]}:\n{nl.join([', '.join(n) for n in active[2]])}")
|
||||||
|
msg=await client.wait_for('message',check=lambda x:x.author.id==active[0][1] and x.channel.id==channel.id and x.content.split(' ')[0] in [n[0] for n in active[2]])
|
||||||
|
action=msg.content.split(' ')
|
||||||
|
try: res=controller.send(action); await channel.send(res)
|
||||||
|
except Exception as ex: await channel.send(str(ex)+'\nThis might have broken the game state; please confirm and quit if so')
|
||||||
|
await channel.send(f'```{board}```')
|
||||||
|
await channel.send(end)
|
||||||
|
rancegames.pop(channel.id)
|
||||||
|
|
||||||
|
async def getnp(question,players,formats,conditions):
|
||||||
|
# Each condition is a function that will return a string if the content failed validation.
|
||||||
|
# That string should be fired back to the user, for them to retry.
|
||||||
|
# The conditions should accept a string.
|
||||||
|
solos=[get1p(question,players[i],formats[i],conditions) for i in range(len(players))]
|
||||||
|
return [n.content.replace(' ',' ') for n in await asyncio.gather(*solos)]
|
||||||
|
|
||||||
|
async def get1p(question,player,forma,conditions):
|
||||||
|
# Probably easier to do it like this.
|
||||||
|
await player.send(question.format(*forma))
|
||||||
|
valid=False
|
||||||
|
while not valid:
|
||||||
|
msg=await client.wait_for('message',check=lambda x:x.author.id==player.id and x.channel==x.author.dm_channel)
|
||||||
|
content=msg.content.replace(' ',' ')
|
||||||
|
valid=True
|
||||||
|
for con in conditions:
|
||||||
|
res=con(content)
|
||||||
|
if res!=True: valid=False; await player.send(f"Please try again:\n{res}"); break
|
||||||
|
return msg
|
||||||
|
|
||||||
|
async def rancequit(args, channel, message):
|
||||||
|
# Get noobed
|
||||||
|
try: del rancegames[channel.id]; rancegames.pop(channel.id)
|
||||||
|
except KeyError: pass
|
||||||
|
|
||||||
|
# I might make this use the playrance function later.
|
||||||
|
async def rancehack(args, channel, message):
|
||||||
|
rancegames[channel.id]=board=automate.Board('a','b','','',{0:automate.unit.BattleUnit('tactician',1,9,3,9,400,4,{},'Ram'),2:automate.unit.BattleUnit('tactician',1,9,3,3,400,4,{},'Rem')},{4:automate.unit.BattleUnit('tactician',1,9,3,3,400,4,{},'Rom'),
|
||||||
|
2:automate.unit.BattleUnit('tactician',1,9,3,3,400,4,{},'Rim')})
|
||||||
|
board.turn=2
|
||||||
|
controller=board.controller()
|
||||||
|
nl='\n'
|
||||||
|
while True:
|
||||||
|
if not rancestatus(channel.id,board): return
|
||||||
|
# We can discard this during charged actions but if we don't get it shit goes wrong.
|
||||||
|
try: active=next(controller)
|
||||||
|
except StopIteration as ex: end=ex; break
|
||||||
|
if active is None:
|
||||||
|
# Charged action is happening. Don't wait for action or anything.
|
||||||
|
action=None
|
||||||
|
else:
|
||||||
|
await channel.send(f'```{board}```')
|
||||||
|
await channel.send(f"Available actions for {active[0][0]}'s unit {active[1]}:\n{nl.join([', '.join(n) for n in active[2]])}")
|
||||||
|
msg=await client.wait_for('message',check=lambda x:x.author!=client.user and x.channel.id==channel.id and x.content.split(' ')[0] in [n[0] for n in active[2]])
|
||||||
|
action=msg.content.split(' ')
|
||||||
|
res=controller.send(action); await channel.send(res)
|
||||||
|
#except Exception as ex: await channel.send(str(ex)+'\nThis might have broken the game state; please confirm and quit if so')
|
||||||
|
await channel.send(f'```{board}```')
|
||||||
|
await channel.send(end)
|
||||||
|
rancegames.pop(channel.id)
|
||||||
|
|
||||||
|
def rancestatus(cid,seed):
|
||||||
|
return cid in rancegames and rancegames[cid]==seed
|
||||||
|
|
||||||
|
async def azul(args, channel, message):
|
||||||
|
pass
|
16
skill.py
Normal file
16
skill.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
slot=Enum("Slot",["Buff","Attack","Special","Rest","Passive","Class"])
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Skill():
|
||||||
|
name:str
|
||||||
|
slot:slot
|
||||||
|
hooks:dict={}
|
||||||
|
def __getitem__(self,item):
|
||||||
|
return self.hooks.get(item)
|
||||||
|
|
||||||
|
attack=Skill('attack',slot.Attack,{'action':actions.attack})
|
||||||
|
shikigami=Skill('shikigami',slot.Attack,{'action':actions.allattack})
|
||||||
|
retaliate=Skill('retaliate',slot.Class,{'attacked':reactions.counter})
|
20
todo.txt
Normal file
20
todo.txt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Hooks:
|
||||||
|
- Battle start (tactician, foot soldier)
|
||||||
|
- Battle end (nobody?)
|
||||||
|
- Turn start (nobody?)
|
||||||
|
- Turn end (Rance)
|
||||||
|
- Attack (everyone)
|
||||||
|
- Attacked (everyone)
|
||||||
|
- Ally attacked (foot soldier)
|
||||||
|
- Foe attacked (nobody?)
|
||||||
|
- Ally defeated (nobody?)
|
||||||
|
- Foe defeated (defeated warrior hunt)
|
||||||
|
- Action (everyone)
|
||||||
|
|
||||||
|
Things that can hook:
|
||||||
|
- Unit's class
|
||||||
|
- Unit's skills
|
||||||
|
- Unit's item
|
||||||
|
- Unit's status
|
||||||
|
|
||||||
|
Summon ogre costs 2 flags
|
100
unit.py
Normal file
100
unit.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import json as j
|
||||||
|
from os.path import dirname,join
|
||||||
|
with open(join(dirname(__file__),'data.json')) as d:
|
||||||
|
data=j.load(d)
|
||||||
|
classtable=data['classes']
|
||||||
|
statii=data['statii']
|
||||||
|
from . import actions
|
||||||
|
|
||||||
|
class RosterUnit():
|
||||||
|
def __init__(self,cls,atk,dfn,inti,spd,size,flags,actis={},name=None):
|
||||||
|
self.cls=cls
|
||||||
|
self.atk=atk;self.dfn=dfn;self.int=inti;self.spd=spd
|
||||||
|
self.max=size
|
||||||
|
self.cur=size
|
||||||
|
self.flags=flags
|
||||||
|
self.name=name or ''
|
||||||
|
# Actions that I can't be assed aliasing will just be passed in as a list in the key 0.
|
||||||
|
actis.update({k:k for k in actis.pop(0,[])})
|
||||||
|
self.actions=actis or {n:getattr(actions,n) for n in classtable[cls]['actions']} # This needs altering.
|
||||||
|
self.actions['rest']=actions.rest
|
||||||
|
self.available=True
|
||||||
|
self.affection=[0,0] # This should really be a tuple but it can change over time, and it would be annoying to set it instead of just modify it. The two values mean different things, you see.
|
||||||
|
|
||||||
|
#This is way too much writing. I should find a way to alias it.
|
||||||
|
@property
|
||||||
|
def atk(self): return self._atk
|
||||||
|
@atk.setter
|
||||||
|
def atk(self,val): self._atk=val
|
||||||
|
@property
|
||||||
|
def dfn(self): return self._def
|
||||||
|
@dfn.setter
|
||||||
|
def dfn(self,val): self._def=val
|
||||||
|
@property
|
||||||
|
def int(self): return self._int
|
||||||
|
@int.setter
|
||||||
|
def int(self,val): self._int=val
|
||||||
|
@property
|
||||||
|
def spd(self): return self._spd
|
||||||
|
@spd.setter
|
||||||
|
def spd(self,val): self._spd=val
|
||||||
|
|
||||||
|
def rebuild(self):
|
||||||
|
return f"RosterUnit('{self.cls}',{self.atk},{self.dfn},{self.int},{self.spd},{self.max},{self.flags},{self.actions},'{self.name}')"
|
||||||
|
|
||||||
|
def static(self):
|
||||||
|
return f"{self.name}: {self.cls}, ATK: {self.atk}, DEF: {self.dfn}, INT: {self.int}, SPD: {self.spd}, TROOPS: {self.max}, FLAGS: {self.flags}"
|
||||||
|
def __str__(self): return self.static() # This should be overridden but the static method should continue to be available.
|
||||||
|
|
||||||
|
class BattleUnit(RosterUnit):
|
||||||
|
def __init__(self,*args,code=None,board=None,**kwargs):
|
||||||
|
super().__init__(*args,**kwargs)
|
||||||
|
self.curflags=self.flags
|
||||||
|
self.code=code
|
||||||
|
self.board=board
|
||||||
|
# Fuck the Status class
|
||||||
|
self.status={}
|
||||||
|
|
||||||
|
@RosterUnit.atk.getter
|
||||||
|
def atk(self): return self._atk*classtable[self.cls]['atk']*(1+self.status.get('atk',0))
|
||||||
|
@RosterUnit.dfn.getter
|
||||||
|
def dfn(self): return self._def*classtable[self.cls]['def']*(1+self.status.get('def',0))
|
||||||
|
@RosterUnit.int.getter
|
||||||
|
def int(self): return self._int*classtable[self.cls]['int']*(1+self.status.get('int',0))
|
||||||
|
@RosterUnit.spd.getter
|
||||||
|
def spd(self): return self._spd*classtable[self.cls]['spd']*(1+self.status.get('spd',0))
|
||||||
|
|
||||||
|
def die(self):
|
||||||
|
self.cur=0
|
||||||
|
self.status['dead']=True
|
||||||
|
self.curflags=0
|
||||||
|
return (100,0)
|
||||||
|
|
||||||
|
def aspd(self,val):
|
||||||
|
# This still has use because of that +4.
|
||||||
|
# Actually maybe I will move it out.
|
||||||
|
return val/(self.spd+4)
|
||||||
|
|
||||||
|
# This is unused now.
|
||||||
|
def bigstatus(self):
|
||||||
|
try: k=list(filter(lambda x:x in self.status,statii))[0]
|
||||||
|
except IndexError: return '' # There is no status
|
||||||
|
v=self.status[k]
|
||||||
|
outtypes={bool:'{k}',float:'{k}: {v:.3}',None:'{k}: {v}'}
|
||||||
|
return outtypes.get(type(v),outtypes[None]).format(k,v).upper()
|
||||||
|
|
||||||
|
def multistatus(self):
|
||||||
|
if 'dead' in self.status: return 'DEAD'
|
||||||
|
out=[n[0] if n in self.status else ' ' for n in statii]
|
||||||
|
if 'g' in out: out[out.index('g')]=f"g{round(self.status['guard']*100)}%"
|
||||||
|
out.append(' '*(13-len(out)))
|
||||||
|
return ''.join(out)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}: Troops: {self.cur}/{self.max}, Flags: {self.curflags}/{self.flags}, Status: {self.status}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tobattle(cls,roster):
|
||||||
|
out=cls(roster.cls,roster.atk,roster.dfn,roster.int,roster.spd, roster.max,roster.flags,actis=roster.actions,name=roster.name)
|
||||||
|
out.cur=roster.cur
|
||||||
|
return out
|
Loading…
Reference in New Issue
Block a user