else/Dodgy/dodgy.py

584 lines
18 KiB
Python

import random
import sys
import tkinter
def clamp_abs(x, clamp):
if x > 0:
return min(x, clamp)
if x < 0:
return max(x, -clamp)
return x
def dist(xa, ya, xb, yb):
#distance formula
result = (xa - xb) **2
result += (ya - yb) ** 2
result = result ** 0.5
result = int(result)
return result
def reduce_to_one(x):
if x < 0:
return -1
if x > 0:
return 1
return 0
class dodgygame:
def __init__(self):
self.tk = tkinter.Tk()
self.tk.resizable(0,0)
self.tk.wm_title('Dodgy')
self.tk.iconbitmap('Excl.ico')
self.arenasize = 40
self.arenasize_walls = self.arenasize + 2
self.player = Player(x=0, y=0)
self.labelframe = tkinter.Frame(self.tk)
self.labelframe.grid(row=1, column=0, columnspan=100)
#Allows me to center the gui units
self.datalabel = tkinter.Label(self.tk, text='', font=('Terminal', 10))
self.datalabel.grid(row=0, column=0, columnspan=100)
self.stepslabel = tkinter.Label(self.labelframe, text='0', font=('Consolas', 10))
self.stepslabel.grid(row=0, column=0)
self.collectlabel = tkinter.Label(self.labelframe, text='0', font=('Consolas', 10), bg='#ccc')
self.collectlabel.grid(row=0, column=1)
self.bomblabel = tkinter.Label(self.labelframe, text='0', font=('Consolas', 10))
self.bomblabel.grid(row=0, column=2)
self.phantlabel = tkinter.Label(self.labelframe, text='0', font=('Consolas', 10), bg='#ccc')
self.phantlabel.grid(row=0, column=3)
self.poslabel = tkinter.Label(self.labelframe, text='', font=('Consolas', 10))
self.poslabel.grid(row=0, column=4)
self.helplabel = tkinter.Label(self.tk, text='Press H for help ->', font=('Consolas', 8))
self.helplabel.grid(row=2, column=0, columnspan=100)
self.consolelabel = tkinter.Label(self.tk, text='Welcome', font=('Terminal', 8), width=self.arenasize_walls, height=4)
self.consolelabeldata = []
self.consolelabel.configure(anchor='nw', justify='left')
self.consolelabel.grid(row=3, column=0)
self.tk.bind('w', lambda *args: self.move_up())
self.tk.bind('<Up>', lambda *args: self.move_up())
self.tk.bind('s', lambda *args: self.move_down())
self.tk.bind('<Down>', lambda *args: self.move_down())
self.tk.bind('a', lambda *args: self.move_left())
self.tk.bind('<Left>', lambda *args: self.move_left())
self.tk.bind('d', lambda *args: self.move_right())
self.tk.bind('<Right>', lambda *args: self.move_right())
self.tk.bind('<Button-1>', self.move_by_mouse)
self.tk.bind('<Button-2>', lambda *args: self.middlemouse())
self.tk.bind('<Button-3>', lambda *args: self.spawnbomb())
self.tk.bind('j', lambda *args: self.spawnbomb())
self.tk.bind('z', lambda *args: self.spawnbomb())
self.tk.bind('k', lambda *args: self.spawnphantom())
self.tk.bind('x', lambda *args: self.spawnphantom())
self.tk.bind('r', lambda *args: self.restart())
self.tk.bind('h', lambda *args: self.helpreel())
self.tk.bind('<Control-w>', lambda *args: self.tk.quit())
self.candylist = []
self.enemylist = []
self.bomblist = []
self.phantomlist = []
self.goldcandylist = []
self.entlist = []
self.symbol_wall ='#'
self.symbol_floor = ' '
self.candyspawnrate = 4
self.enemyspawnrate = 50
self.enemyspawnmindist = 9
self.enemydocollide = False
self.bombcost = 4
self.phantomcost = 60
self.goldcandyflashrate = 8
self.goldcandyspawnrate = 40
self.helplabelindex = -1
self.helplabeltexts = [
'<WASD>=Movement <J>=Bomb <K>=Phantom <R>=Restart ->',
'<UDLR>=Movement <Z>=Bomb <X>=Phantom <R>=Restart ->',
'<LMB>=Movement <RMB>=Bomb <MMB>=Phantom/Restart ->',
f'{Player.symbol} = You ->',
f'{Enemy.symbol} = Exclamator ->',
f'{Candy.symbol} = Candy ->',
f'{Bomb.symbol} = Bomb ->',
'Avoid the Exclamators ->',
'Collect candy to earn bombs ->',
'Drop bombs to snare Exclamators ->',
'Deploy phantoms to distract Exclamators ->',
f'Phantoms have a minimum cost of {self.phantomcost} ->',
'But deploying phantom will consume all candy ->',
'More candy consumed = longer phantom lifespan ->',
'Enjoy •',
'Press H for help ->',
]
self.restart()
self.tk.update()
width = self.tk.winfo_reqwidth()
height = self.tk.winfo_reqheight()
x_offset = (self.tk.winfo_screenwidth() - width) / 2
y_offset = (self.tk.winfo_screenheight() - height) / 2
self.tk.geometry('%dx%d+%d+%d' % (width, height, x_offset, y_offset-50))
self.tk.mainloop()
def collect(self, score):
for x in range(score):
self.collections += 1
if self.collections % self.bombcost == 0:
self.bombs += 1
def do_enemy_bomb_collisions(self):
for bomb in list(self.bomblist):
for enemy in list(self.enemylist):
if enemy.x == bomb.x and enemy.y == bomb.y:
self.bomblist.remove(bomb)
self.enemylist.remove(enemy)
self.entlist.remove(bomb)
self.entlist.remove(enemy)
self.printr(f'[ {Bomb.symbol} ] Bang')
break
def do_player_enemy_collisions(self):
for enemy in list(self.enemylist):
if enemy.x == self.player.x and enemy.y == self.player.y:
self.printr(f'[ {Player.symbol} ] Death')
self.isdead = True
self.datalabel.configure(fg='Red')
def do_player_item_collisions(self):
for entity in list(self.entlist):
if entity.xy == self.player.xy:
if isinstance(entity, Candy):
self.collect(entity.value)
self.candylist.remove(entity)
self.entlist.remove(entity)
elif isinstance(entity, Goldcandy):
self.printr(f'[ {Goldcandy.symbol} ] Got gold')
self.collect(entity.value)
self.goldcandylist.remove(entity)
self.entlist.remove(entity)
def _game_tick(self):
self.do_player_item_collisions()
self.do_player_enemy_collisions()
if self.isdead:
return
self.move_enemies()
self.do_player_enemy_collisions()
if self.isdead:
return
self.do_enemy_bomb_collisions()
self.ticks_elapsed += 1
if self.ticks_elapsed % self.candyspawnrate == 0:
self.spawncandy()
if self.ticks_elapsed % self.goldcandyspawnrate == 0 and random.random() < 0.75:
self.spawngoldcandy()
if self.ticks_elapsed % self.enemyspawnrate == 0:
enemy_count = (self.ticks_elapsed // 200) + 1
for x in range(enemy_count):
self.spawnenemy()
self.expire_goldcandies()
self.expire_phantoms()
def game_tick(self):
self._game_tick()
self.redraw()
def expire_goldcandies(self):
for goldcandy in list(self.goldcandylist):
goldcandy.tick(self.goldcandyflashrate)
if goldcandy.lifespan <= 0:
self.goldcandylist.remove(goldcandy)
self.entlist.remove(goldcandy)
def expire_phantoms(self):
for phantom in list(self.phantomlist):
if phantom.lifespan <= 0:
self.phantomlist.remove(phantom)
self.entlist.remove(phantom)
def helpreel(self):
self.helplabelindex += 1
if self.helplabelindex > (len(self.helplabeltexts) - 1):
self.helplabelindex = 0
self.helplabel.configure(text=self.helplabeltexts[self.helplabelindex])
def middlemouse(self):
if self.isdead:
self.restart()
else:
self.spawnphantom()
def move_by_mouse(self, event):
event.x -= 9
event.y -= 9
event.x /= 8
event.y /= 12
xdif = event.x - self.player.x
ydif = event.y - self.player.y
if abs(xdif) >= 0.5 or abs(ydif) >= 0.5:
if abs(xdif) >= abs(ydif):
if xdif > 0:
self.move_right()
else:
self.move_left()
else:
if ydif > 0:
self.move_down()
else:
self.move_up()
def move_enemies(self):
for enemy in self.enemylist:
oldx = enemy.x
oldy = enemy.y
if self.phantomlist == []:
enemy.approach(self.player.x, self.player.y)
else:
ph = self.phantomlist[0]
enemy.approach(ph.x, ph.y)
if self.enemydocollide:
for otheren in self.enemylist:
if enemy != otheren:
if enemy.xy == otheren.xy:
self.printr(f'[ {Enemy.symbol} ] Enemy collision at {enemy.x} {enemy.y}')
enemy.x = oldx
enemy.y = oldy
if enemy.x < 1:
enemy.x = 1
if enemy.x > (self.arenasize):
enemy.x = self.arenasize
if enemy.y < 1:
enemy.y = 1
if enemy.y > (self.arenasize):
enemy.y = self.arenasize
def move_left(self):
if self.isdead:
return
if self.player.x > 1:
self.player.x -= 1
self.stepstaken += 1
for phantom in self.phantomlist:
if phantom.x < self.arenasize:
phantom.x += 1
phantom.lifespan -= 1
self.game_tick()
def move_right(self):
if self.isdead:
return
if self.player.x < self.arenasize:
self.player.x += 1
self.stepstaken += 1
for phantom in self.phantomlist:
if phantom.x > 1:
phantom.x -= 1
phantom.lifespan -= 1
self.game_tick()
def move_up(self):
if self.isdead:
return
if self.player.y > 1:
self.player.y -= 1
self.stepstaken += 1
for phantom in self.phantomlist:
if phantom.y < self.arenasize:
phantom.y += 1
phantom.lifespan -= 1
self.game_tick()
def move_down(self):
if self.isdead:
return
if self.player.y < self.arenasize:
self.player.y += 1
self.stepstaken += 1
for phantom in self.phantomlist:
if phantom.y > 1:
phantom.y -= 1
phantom.lifespan -= 1
self.game_tick()
def printr(self, info):
print(info)
self.consolelabeldata.append(str(info))
self.consolelabeldata = self.consolelabeldata[-4:]
self.consolelabel.configure(text='\n'.join(self.consolelabeldata))
def redraw(self):
display = []
display.append([self.symbol_wall] * self.arenasize_walls)
for y in range(self.arenasize):
display.append([self.symbol_wall] + [self.symbol_floor] * self.arenasize + [self.symbol_wall])
display.append([self.symbol_wall] * self.arenasize_walls)
if not self.isdead:
display[self.player.y][self.player.x] = Player.symbol
self.entlist.sort(key=lambda ent: ent.precedence, reverse=True)
for entity in self.entlist:
display[entity.y][entity.x] = entity.symbol
display = '\n'.join(''.join(line) for line in display)
self.datalabel.configure(text=display)
self.stepslabel.configure(text=f'{self.stepstaken:04d} steps')
self.collectlabel.configure(text=f'{self.collections:03d} candy')
self.bomblabel.configure(text=f'{self.bombs:02d} bombs')
self.poslabel.configure(text=f'{self.player.x:02d} {self.player.y:02d}')
if self.collections >= self.phantomcost:
self.phantlabel.configure(text='1 phantom')
else:
self.phantlabel.configure(text='0 phantom')
def restart(self):
self.printr('Resetting game.')
self.consolelabeldata = []
self.consolelabel.configure(text='')
self.player.x = self.arenasize // 2
self.player.y = self.arenasize // 2
self.entlist = []
self.candylist = []
self.goldcandylist = []
self.enemylist = []
self.bomblist = []
self.phantomlist = []
self.ticks_elapsed = 0
self.stepstaken = 0
self.collections = 0
self.bombs = 0
self.isdead = False
self.datalabel.configure(fg='Black')
self.spawncandy()
self.game_tick()
def spawnbomb(self):
goodtogo = True
for bomb in self.bomblist:
if bomb.xy == self.player.xy:
goodtogo = False
if goodtogo:
if self.bombs > 0:
self.bombs -= 1
newbomb = Bomb(self.player.x, self.player.y)
self.bomblist.append(newbomb)
self.entlist.append(newbomb)
self.redraw()
def spawncandy(self, forcex=None, forcey=None):
if forcex is None or forcey is None:
newx = random.randint(1, self.arenasize)
newy = random.randint(1, self.arenasize)
else:
newx = forcex
newy = forcey
new_candy = Candy(newx, newy)
if self.player.xy == new_candy.xy:
return
for candy in self.candylist:
if candy.xy == new_candy.xy:
return
self.printr(f'[ {Candy.symbol} ] New candy at {newx} {newy}')
self.candylist.append(new_candy)
self.entlist.append(new_candy)
def spawnenemy(self):
newx = random.randint(1, self.arenasize)
newy = random.randint(1, self.arenasize)
spawntries = 0
while (dist(self.player.x, self.player.y, newx, newy) < self.enemyspawnmindist):
newx = random.randint(1, self.arenasize)
newy = random.randint(1, self.arenasize)
spawntries += 1
#print('Rerolling from', newx, newy)
if spawntries == 10:
print('Could not spawn enemy')
return
new_enemy = Enemy(newx, newy, 1)
for enemy in self.enemylist:
if enemy.xy == new_enemy.xy:
return
self.printr(f'[ {Enemy.symbol} ] New enemy at {newx} {newy}')
self.enemylist.append(new_enemy)
self.entlist.append(new_enemy)
def spawngoldcandy(self, forcex=None, forcey=None):
if forcex is None or forcey is None:
newx = random.randint(1, self.arenasize)
newy = random.randint(1, self.arenasize)
spawntries = 0
while (dist(self.player.x, self.player.y, newx, newy) < self.enemyspawnmindist):
newx = random.randint(1, self.arenasize)
newy = random.randint(1, self.arenasize)
spawntries += 1
#print('Rerolling from', newx, newy)
if spawntries == 20:
#print('Could not spawn enemy')
return
else:
newx = forcex
newy = forcey
lifespan = dist(self.player.x, self.player.y, newx, newy)
lifespan *= 2
new_candy = Goldcandy(newx, newy, lifespan)
for entity in self.entlist:
if entity.xy == new_candy.xy:
return
self.printr(f'[ {Goldcandy.symbol} ] New gold candy')
self.goldcandylist.append(new_candy)
self.entlist.append(new_candy)
def spawnphantom(self):
goodtogo = True
if self.collections < self.phantomcost:
self.printr(f'[ {Player.symbol} ] Not enough Candy. {self.collections} / {self.phantomcost}.')
goodtogo = False
if self.phantomlist != []:
goodtogo = False
if goodtogo:
life = 15
self.collections = 0
life += round(self.collections / 3)
self.printr(f'[ {Player.symbol} ] New phantom with {life} life.')
newphantom = Phantom(self.player.x, self.player.y, life)
self.phantomlist.append(newphantom)
self.entlist.append(newphantom)
self.redraw()
class Entity:
def __init__(self, x, y):
self.x = x
self.y = y
@property
def xy(self):
return (self.x, self.y)
class Bomb(Entity):
symbol = 'X'
class Candy(Entity):
symbol = '@'
value = 1
class Enemy(Entity):
symbol = '!'
def __init__(self, x, y, anger):
super().__init__(x, y)
self.anger = anger
self.vel_x = 0
self.vel_y = 0
def approach(self, targx, targy):
accel_x = targx - self.x
accel_y = targy - self.y
self.vel_x = clamp_abs(self.vel_x + accel_x, 5) * 0.85
self.vel_y = clamp_abs(self.vel_y + accel_y, 5) * 0.85
mag_x = abs(self.vel_x)
mag_y = abs(self.vel_y)
if mag_x >= 1 and mag_x > mag_y:
self.x += reduce_to_one(self.vel_x)
if mag_y >= 1 and mag_y >= mag_x:
self.y += reduce_to_one(self.vel_y)
class Goldcandy(Entity):
symbol = '$'
value = 5
def __init__(self, x, y, lifespan):
super().__init__(x, y)
self.lifespan = lifespan
self.symbol = Goldcandy.symbol
def tick(self, flashrate):
self.lifespan -= 1
if self.lifespan % flashrate == 0 or self.lifespan % flashrate == 1:
self.symbol = Goldcandy.symbol
else:
self.symbol = Candy.symbol
class Player(Entity):
symbol = 'H'
class Phantom(Entity):
symbol = 'H'
def __init__(self, x, y, lifespan):
super().__init__(x, y)
self.lifespan = lifespan
ENTITY_PRECEDENCE = [
Bomb,
Player,
Enemy,
Phantom,
Goldcandy,
Candy,
]
for (index, cls) in enumerate(ENTITY_PRECEDENCE):
cls.precedence = index
def main(argv):
dodgygame()
return 0
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))