Advertisement
Guest User

Minimal Roguelike in Pygame

a guest
Aug 31st, 2024
208
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.49 KB | Source Code | 0 0
  1.  
  2. import random
  3. import numpy as np
  4. import pygame as pg
  5. import tcod
  6.  
  7. SCREEN_SHAPE = (1280, 720)
  8. MAP_SHAPE = (60, 60)
  9. TILE_SIZE = 32
  10. FPS = 60
  11. N_ENEMIES = 5
  12. MOVE_KEYS = {
  13.     pg.K_UP: (0, -1),
  14.     pg.K_DOWN: (0, 1),
  15.     pg.K_LEFT: (-1, 0),
  16.     pg.K_RIGHT: (1, 0),
  17.     pg.K_w: (0, -1),
  18.     pg.K_s: (0, 1),
  19.     pg.K_a: (-1, 0),
  20.     pg.K_d: (1, 0),
  21.     pg.K_q: (-1, -1),
  22.     pg.K_e: (1, -1),
  23.     pg.K_z: (-1, 1),
  24.     pg.K_c: (1, 1)
  25. }
  26. WAIT_KEYS = (pg.K_RETURN, pg.K_SPACE, pg.K_PERIOD)
  27.  
  28. class WaitAction:
  29.     def can(self):
  30.         return True
  31.  
  32.     def perform(self):
  33.         return self
  34.  
  35. class MoveAction:
  36.     def __init__(self, dx, dy, actor):
  37.         self.dx, self.dy, self.actor = dx, dy, actor
  38.  
  39.     def can(self):
  40.         dist = (self.dx**2 + self.dy**2)**0.5
  41.         if dist > 1.5:
  42.             return False
  43.         new_x = self.actor.x + self.dx
  44.         new_y = self.actor.y + self.dy
  45.         if new_x < 0 or new_y < 0 or new_x >= MAP_SHAPE[0] or new_y >= MAP_SHAPE[1]:
  46.             return False
  47.         return self.actor.game_logic.is_walkable(new_x, new_y)
  48.    
  49.     def perform(self):
  50.         if not self.can():
  51.             return
  52.         self.actor.x += self.dx
  53.         self.actor.y += self.dy
  54.         return self
  55.  
  56. class AttackAction:
  57.     def __init__(self, target, actor):
  58.         self.target = target
  59.         self.actor = actor
  60.  
  61.     def can(self):
  62.         dist = ((self.target.x-self.actor.x)**2+(self.target.y-self.actor.y)**2)**0.5
  63.         if dist > 1.5:
  64.             return False
  65.         if self.target.hp < 1:
  66.             return False
  67.         return True
  68.  
  69.     def perform(self):
  70.         if not self.can():
  71.             return None
  72.         roll = random.randint(1, 20) + self.actor.tohit
  73.         if isinstance(self.actor, Player):
  74.             text = "You attack the enemy: "
  75.         else:
  76.             text = "The enemy attacks you: "
  77.         if roll >= self.target.ac:
  78.             self.damage = random.randint(1, self.actor.damage)
  79.             self.target.hp = max(0, self.target.hp - self.damage)
  80.             text += f"{self.damage} points of damage!"
  81.         else:
  82.             self.damage = 0
  83.             text += "Miss!"
  84.         self.actor.game_logic.log(text)
  85.         if self.target.hp < 0:
  86.             if isinstance(self.target, Player):
  87.                 self.actor.game_logic.log("You die!")
  88.             else:
  89.                 self.actor.game_logic.log("The enemy dies!")
  90.         return self
  91.  
  92. class BumpAction:
  93.     def __init__(self, dx, dy, actor):
  94.         self.dx, self.dy, self.actor = dx, dy, actor
  95.  
  96.     def get_entity(self):
  97.         new_x = self.actor.x + self.dx
  98.         new_y = self.actor.y + self.dy
  99.         for e in self.actor.game_logic.entities:
  100.             if e.x == new_x and e.y == new_y and e != self.actor:
  101.                 return e
  102.         return None
  103.  
  104.     def can(self):
  105.         move = MoveAction(self.dx, self.dy, self.actor)
  106.         if move.can():
  107.             return True
  108.         entity = self.get_entity()
  109.         return entity is not None
  110.  
  111.     def perform(self):
  112.         if not self.can():
  113.             return None
  114.         move = MoveAction(self.dx, self.dy, self.actor)
  115.         if move.can():
  116.             return move.perform()
  117.         entity = self.get_entity()
  118.         attack = AttackAction(entity, self.actor)
  119.         return attack.perform()
  120.  
  121. class Entity:
  122.     def __init__(self, game_logic, x, y, sprite, row, col):
  123.         self.game_logic = game_logic
  124.         self.x, self.y = x, y
  125.         self.sprite, self.row, self.col = sprite, row, col
  126.         self.max_hp = 10
  127.         self.hp = 10
  128.         self.tohit = 4
  129.         self.damage = 6
  130.         self.ac = 12
  131.         self.fov_radius = 5
  132.         self.update_fov()
  133.  
  134.     def update_fov(self):
  135.         transparency = self.game_logic.map!=0
  136.         self.fov = tcod.map.compute_fov(
  137.             transparency, (self.x, self.y), self.fov_radius,
  138.             algorithm=tcod.constants.FOV_SYMMETRIC_SHADOWCAST)
  139.  
  140. class Player(Entity):
  141.     def __init__(self, game_logic, x, y):
  142.         super().__init__(game_logic, x, y, '32rogues/rogues.png', 1, 1)
  143.         self.max_hp = 40
  144.         self.hp = 40
  145.  
  146.     def update_fov(self):
  147.         super().update_fov()
  148.         self.game_logic.explored |= self.fov
  149.  
  150. class Enemy(Entity):
  151.     def __init__(self, game_logic, x, y):
  152.         super().__init__(game_logic, x, y, '32rogues/monsters.png', 0, 0)
  153.  
  154.     def next_action(self):
  155.         player = self.game_logic.player
  156.         px, py = player.x, player.y
  157.         dist = ((px-self.x)**2 + (py-self.y)**2)**0.5
  158.         if player.hp < 1 or not self.fov[px, py]:
  159.             dx, dy = random.randint(-1, 1), random.randint(-1, 1)
  160.             return MoveAction(dx, dy, self)
  161.         if dist < 1.5:
  162.             return AttackAction(player, self)
  163.         path = self.game_logic.astar_path((self.x, self.y), (player.x, player.y))
  164.         if len(path) < 2:
  165.             return WaitAction()
  166.         dx = path[1][0] - path[0][0]
  167.         dy = path[1][1] - path[0][1]
  168.         return MoveAction(dx, dy, self)
  169.  
  170. class GameLogic:
  171.     def __init__(self, interface):
  172.         self.entities = []
  173.         self.current_turn = -1
  174.         self.interface = interface
  175.         self.input_action = None
  176.         self.message_log = []
  177.         self.last_action = None
  178.         self.init_map()
  179.         self.init_player()
  180.    
  181.     def log(self, text):
  182.         self.message_log.append(text)
  183.  
  184.     def init_map(self):
  185.         self.map = np.zeros(MAP_SHAPE)
  186.         for walkers in range(5):
  187.             x, y = (MAP_SHAPE[0] // 2, MAP_SHAPE[1] // 2)
  188.             self.map[x, y] = 1
  189.             for iterators in range(500):
  190.                 dx, dy = random.choice([(0,1), (1,0), (0,-1), (-1,0)])
  191.                 if x+dx>0 and y+dy>0 and x+dx<MAP_SHAPE[0]-1 and y+dy<MAP_SHAPE[1]-1:
  192.                     x += dx
  193.                     y += dy
  194.                     self.map[x, y] = 1
  195.                 else:
  196.                     break
  197.         self.explored = np.zeros(MAP_SHAPE)!=0
  198.  
  199.     def init_player(self):
  200.         x, y = np.where(self.map == 1)
  201.         i = list(range(len(x)))
  202.         random.shuffle(i)
  203.         self.player = Player(self, x[i[0]], y[i[0]])
  204.         self.entities.append(self.player)
  205.         for k in range(1, N_ENEMIES+1):
  206.             enemy = Enemy(self, x[i[k]], y[i[k]])
  207.             self.entities.append(enemy)
  208.  
  209.     def update(self):
  210.         if self.current_turn >= len(self.entities):
  211.             self.current_turn = 0
  212.         entity = self.entities[self.current_turn]
  213.         action = None
  214.         if entity.hp < 1:
  215.             self.entities.remove(entity)
  216.             return
  217.         if isinstance(entity, Player):
  218.             if self.input_action is None:
  219.                 return
  220.             else:
  221.                 action = self.input_action
  222.         else:
  223.             action = entity.next_action()
  224.         if action is not None:
  225.             self.last_action = action.perform()
  226.         self.input_action = None
  227.         self.current_turn += 1
  228.         if self.current_turn >= len(self.entities):
  229.             self.current_turn = 0
  230.         self.entities[self.current_turn].update_fov()
  231.  
  232.     def astar_path(self, origin, target):
  233.         cost = self.map.copy()
  234.         for e in self.entities:
  235.             cost[e.x, e.y] = 0
  236.         cost[origin[0], origin[1]] = 1
  237.         cost[target[0], target[1]] = 1
  238.         graph = tcod.path.SimpleGraph(cost=cost.astype(np.int8), cardinal=5, diagonal=7)
  239.         pathfinder = tcod.path.Pathfinder(graph)
  240.         pathfinder.add_root(origin)
  241.         return pathfinder.path_to(target).tolist()
  242.  
  243.     def is_walkable(self, x, y):
  244.         if self.map[x, y] == 0:
  245.             return False
  246.         for e in self.entities:
  247.             if e.x == x and e.y == y:
  248.                 return False
  249.         return True
  250.  
  251.  
  252. # RENDERING ==========================================
  253.  
  254. class EntitySprite(pg.sprite.Sprite):
  255.     def __init__(self, group, interface, game_logic, entity):
  256.         super().__init__(group)
  257.         self.group = group
  258.         self.entity = entity
  259.         self.game_logic = game_logic
  260.         self.interface = interface
  261.         self.is_in_fov = None
  262.         tilesheet = pg.image.load(self.entity.sprite).convert_alpha()
  263.         self.tile = tilesheet.subsurface(
  264.             pg.Rect(self.entity.row*TILE_SIZE, self.entity.col*TILE_SIZE,
  265.                 TILE_SIZE, TILE_SIZE))
  266.         self.image = self.tile
  267.         self.hpbar = MapHPBar(group, self)
  268.  
  269.     def update(self):
  270.         if self.entity.hp < 1:
  271.             self.kill()
  272.             return
  273.         x, y = self.interface.grid_to_screen(self.entity.x, self.entity.y)
  274.         self.rect = pg.Rect(x, y, TILE_SIZE, TILE_SIZE)
  275.         is_in_fov = self.game_logic.player.fov[self.entity.x, self.entity.y]
  276.         if is_in_fov == self.is_in_fov:
  277.             return
  278.         self.is_in_fov = is_in_fov
  279.         if is_in_fov:
  280.             self.image = self.tile
  281.         else:
  282.             self.image = pg.Surface((1,1)).convert_alpha()
  283.             self.image.fill("#00000000")
  284.  
  285. class TileSprite(pg.sprite.Sprite):
  286.     def __init__(self, group, interface, game_logic, x, y):
  287.         super().__init__(group)
  288.         self.group = group
  289.         self.x, self.y = x, y
  290.         self.game_logic = game_logic
  291.         self.interface = interface
  292.         self.is_explored = False
  293.         self.is_in_fov = False
  294.         self.is_walkable = None
  295.         self.image = pg.Surface((TILE_SIZE, TILE_SIZE)).convert_alpha()
  296.         self.image.fill("#00000000")
  297.         self.wall = interface.tilesheet.subsurface((TILE_SIZE, TILE_SIZE, TILE_SIZE, TILE_SIZE))
  298.         self.wall2 = interface.tilesheet.subsurface((0, TILE_SIZE, TILE_SIZE, TILE_SIZE))
  299.    
  300.     def update(self):
  301.         x, y = self.interface.grid_to_screen(self.x, self.y)
  302.         self.rect = pg.Rect(x, y, TILE_SIZE, TILE_SIZE)
  303.  
  304.         is_walkable = self.game_logic.map[self.x, self.y] == 1
  305.         is_explored = self.game_logic.explored[self.x, self.y]
  306.         is_in_fov = self.game_logic.player.fov[self.x, self.y]
  307.         if is_explored == self.is_explored and is_in_fov == self.is_in_fov and is_walkable == self.is_walkable:
  308.             return
  309.         self.is_walkable = is_walkable
  310.         self.is_explored = is_explored
  311.         self.is_in_fov = is_in_fov
  312.         k = 255
  313.         if not is_in_fov:
  314.             k //= 2
  315.         if not is_explored:
  316.             k *= 0
  317.         if not is_walkable:
  318.             walkable_below = (self.y < MAP_SHAPE[1] - 1) and (self.game_logic.map[self.x, self.y+1] == 1)
  319.             if walkable_below:
  320.                 self.image.blit(self.wall, (0,0))
  321.             else:
  322.                 self.image.blit(self.wall2, (0,0))
  323.         else:
  324.             self.image.fill("#404040")
  325.         self.image.set_alpha(k)
  326.  
  327. class MapHPBar(pg.sprite.Sprite):
  328.     def __init__(self, group, parent):
  329.         super().__init__(group)
  330.         self.parent = parent
  331.         self.fill = 0
  332.         self.is_in_fov = None
  333.  
  334.     def update(self):
  335.         x, y = self.parent.rect.x, self.parent.rect.bottom
  336.         w, h = self.parent.rect.width, 4
  337.         self.rect = pg.Rect(x, y, w, h)
  338.         entity = self.parent.entity
  339.         if entity.hp < 1:
  340.             self.kill()
  341.         fill = int(self.rect.width * entity.hp / entity.max_hp)
  342.         is_in_fov = self.parent.is_in_fov
  343.         if fill == self.fill and self.is_in_fov == is_in_fov:
  344.             return
  345.         if fill > self.fill:
  346.             self.fill += 1
  347.         elif fill < self.fill:
  348.             self.fill -= 1
  349.         self.parent.is_in_fov = is_in_fov
  350.         self.image = pg.Surface(self.rect.size).convert_alpha()
  351.         if not is_in_fov:
  352.             self.image.fill("#00000000")
  353.             return
  354.         self.image.fill("#808080")
  355.         if self.fill >= self.rect.width // 2:
  356.             color = '#00FF00'
  357.         else:
  358.             color = '#FF0000'
  359.         pg.draw.rect(self.image, color, pg.Rect(0, 0, self.fill, self.rect.height))
  360.  
  361.  
  362. class HPBar(pg.sprite.Sprite):
  363.     def __init__(self, group, game_logic, interface):
  364.         super().__init__(group)
  365.         self.game_logic = game_logic
  366.         self.font: pg.font.Font = interface.font
  367.         self.rect = pg.Rect(16, 16, 200, 20)
  368.         self.fill = 0
  369.  
  370.     def update(self):
  371.         player = self.game_logic.player
  372.         fill = int(self.rect.width * player.hp / player.max_hp)
  373.         if fill == self.fill:
  374.             return
  375.         if fill > self.fill:
  376.             self.fill += 1
  377.         elif fill < self.fill:
  378.             self.fill -= 1
  379.         if self.fill >= self.rect.width // 2:
  380.             color = '#00FF00'
  381.         else:
  382.             color = '#FF0000'
  383.         self.image = pg.Surface(self.rect.size)
  384.         self.image.fill("#808080")
  385.         pg.draw.rect(self.image, color, pg.Rect(0, 0, self.fill, self.rect.height))
  386.         surf = self.font.render(f"{player.hp}/{player.max_hp}", False, '#000000', '#FFFFFF')
  387.         surf.set_colorkey("#FFFFFF")
  388.         self.image.blit(surf, surf.get_rect(center=(self.rect.width//2, self.rect.height//2)))
  389.  
  390.  
  391. class MessageLog(pg.sprite.Sprite):
  392.     def __init__(self, group, game_logic: GameLogic, interface):
  393.         super().__init__(group)
  394.         self.rect = pg.Rect(16, 16+24, SCREEN_SHAPE[0]//2, 24*10)
  395.         self.image = pg.Surface(self.rect.size).convert_alpha()
  396.         self.image.fill("#00000000")
  397.         self.game_logic = game_logic
  398.         self.last_text = None
  399.         self.log_len = 0
  400.         self.font = interface.font
  401.  
  402.     def update(self):
  403.         log_len = len(self.game_logic.message_log)
  404.         if log_len < 1:
  405.             return
  406.         last_text = self.game_logic.message_log[-1]
  407.         if last_text == self.last_text and log_len == self.log_len:
  408.             return
  409.         self.image.fill("#00000000")
  410.         for i in range(1, min(11, log_len)):
  411.             text = self.game_logic.message_log[-i]
  412.             surf = self.font.render(text, False, "#FFFFFF", "#000000")
  413.             surf.set_colorkey("#000000")
  414.             self.image.blit(surf, (0, i*20))
  415.  
  416. class Popup(pg.sprite.Sprite):
  417.     def __init__(self, group, action, interface):
  418.         super().__init__(group)
  419.         self.action = action
  420.         self.counter = 0
  421.         self.interface = interface
  422.         self.x, self.y = self.action.target.x, self.action.target.y
  423.         text = str(self.action.damage)
  424.         if text == '0':
  425.             text = 'MISS'
  426.         self.image = self.interface.font.render(text, False, "#FFFFFF", "#000000")
  427.         self.image.set_colorkey("#000000")
  428.  
  429.     def update(self):
  430.         if self.counter > TILE_SIZE:
  431.             self.kill()
  432.             return
  433.         x, y = self.interface.grid_to_screen(self.x, self.y)
  434.         x += TILE_SIZE // 2
  435.         y -= self.counter // 2
  436.         self.rect = self.image.get_rect(center=(x,y))
  437.         self.counter += 1
  438.  
  439. class Minimap(pg.sprite.Sprite):
  440.     def __init__(self, group, game_logic):
  441.         super().__init__(group)
  442.         self.game_logic = game_logic
  443.         self.scale = 4
  444.         w, h = MAP_SHAPE[0] * self.scale, MAP_SHAPE[1] * self.scale
  445.         x, y = SCREEN_SHAPE[0] - w - 16, 16
  446.         self.rect = pg.Rect(x, y, w, h)
  447.  
  448.     def update(self):
  449.         player = self.game_logic.player
  450.         walkable = self.game_logic.map == 1
  451.         explored = self.game_logic.explored
  452.         fov = player.fov
  453.         grid = np.ones((MAP_SHAPE[0], MAP_SHAPE[1], 3))
  454.         for k in range(3):
  455.             grid[:,:,k] += 120 * explored * walkable
  456.             grid[:,:,k] += 120 * explored * walkable * fov
  457.             grid[:,:,k] += 40 * explored * (walkable==False)
  458.             grid[:,:,k] += 40 * explored * (walkable==False) * fov
  459.         for e in self.game_logic.entities:
  460.             if fov[e.x, e.y]:
  461.                 if isinstance(e, Player):
  462.                     grid[e.x, e.y, :] = [0, 0, 255]
  463.                 else:
  464.                     grid[e.x, e.y, :] = [255, 0, 0]
  465.         self.image = pg.surfarray.make_surface(grid.astype(np.uint8))
  466.         self.image.set_colorkey((1,1,1))
  467.         self.image = pg.transform.scale_by(self.image, self.scale)
  468.  
  469. class GameInterface:
  470.     def __init__(self):
  471.         pg.init()
  472.         self.screen = pg.display.set_mode(SCREEN_SHAPE)
  473.         self.clock = pg.time.Clock()
  474.         self.font = pg.font.Font()
  475.         self.logic = GameLogic(self)
  476.         self.sprite_group = pg.sprite.Group()
  477.         self.ui_group = pg.sprite.Group()
  478.         self.tilesheet = pg.image.load('32rogues/tiles.png').convert_alpha()
  479.         self.hpbar = HPBar(self.ui_group, self.logic, self)
  480.         self.log = MessageLog(self.ui_group, self.logic, self)
  481.         self.minimap = Minimap(self.ui_group, self.logic)
  482.         self.init_sprites()
  483.  
  484.     def init_sprites(self):
  485.         for x in range(MAP_SHAPE[0]):
  486.             for y in range(MAP_SHAPE[1]):
  487.                 TileSprite(self.sprite_group, self, self.logic, x, y)
  488.         for e in self.logic.entities:
  489.             EntitySprite(self.sprite_group, self, self.logic, e)
  490.  
  491.     def grid_to_screen(self, i, j):
  492.         pi, pj = self.logic.player.x, self.logic.player.y
  493.         x = SCREEN_SHAPE[0]//2 + (i-pi) * TILE_SIZE
  494.         y = SCREEN_SHAPE[1]//2 + (j-pj) * TILE_SIZE
  495.         return (x, y)
  496.  
  497.     def screen_to_grid(self, x, y):
  498.         pi, pj = self.logic.player.x, self.logic.player.y
  499.         i = (x - SCREEN_SHAPE[0]//2) // TILE_SIZE + pi
  500.         j = (y - SCREEN_SHAPE[1]//2) // TILE_SIZE + pj
  501.         return (i, j)
  502.  
  503.     def handle_events(self):
  504.         for event in pg.event.get():
  505.             if event.type == pg.QUIT:
  506.                 self.running = False
  507.             elif event.type == pg.KEYDOWN:
  508.                 if event.key in MOVE_KEYS.keys():
  509.                     dx, dy = MOVE_KEYS[event.key]
  510.                     self.logic.input_action = BumpAction(dx, dy, self.logic.player)
  511.                 elif event.key in WAIT_KEYS:
  512.                     self.logic.input_action = WaitAction()
  513.  
  514.     def update(self):
  515.         self.logic.update()
  516.  
  517.     def render(self):
  518.         if isinstance(self.logic.last_action, AttackAction):
  519.             Popup(self.ui_group, self.logic.last_action, self)
  520.             self.logic.last_action = None
  521.         self.sprite_group.update()
  522.         self.ui_group.update()
  523.         self.screen.fill("#000000")
  524.         self.sprite_group.draw(self.screen)
  525.         self.ui_group.draw(self.screen)
  526.         pg.display.flip()
  527.    
  528.     def run(self):
  529.         self.running = True
  530.         while self.running:
  531.             self.delta_time = self.clock.tick(FPS) / 1000
  532.             self.handle_events()
  533.             self.update()
  534.             self.render()
  535.         pg.quit()
  536.  
  537.  
  538. if __name__ == '__main__':
  539.     interface = GameInterface()
  540.     interface.run()
  541.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement