how the hell did I get this far without version control ohno1

This commit is contained in:
Zergling_man 2024-02-17 05:36:54 +11:00
commit 25faf2d550
12 changed files with 980 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__

170
actions.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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