Advertisement
Guest User

Roguelike Tutorial

a guest
Aug 4th, 2017
118
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.11 KB | None | 0 0
  1. import libtcodpy as libtcod
  2. import math
  3. import textwrap
  4. import time
  5.  
  6. # actual size of the window
  7. SCREEN_WIDTH = 150
  8. SCREEN_HEIGHT = 80
  9.  
  10. # size of the map
  11. MAP_WIDTH = 150
  12. MAP_HEIGHT = 73
  13.  
  14. # sizes and coordinates relevant for the GUI
  15. BAR_WIDTH = 20
  16. PANEL_HEIGHT = 7
  17. PANEL_Y = SCREEN_HEIGHT - PANEL_HEIGHT
  18.  
  19. # set the message box constants
  20. MSG_X = BAR_WIDTH + 2
  21. MSG_WIDTH = SCREEN_WIDTH - BAR_WIDTH - 2
  22. MSG_HEIGHT = PANEL_HEIGHT - 1
  23.  
  24. # parameters for dungeon generator
  25. ROOM_MAX_SIZE = 15
  26. ROOM_MIN_SIZE = 6
  27. MAX_ROOMS = 30
  28. MAX_ROOM_MONSTERS = 3
  29.  
  30. # FOV algorithm
  31. FOV_ALGO = 0
  32.  
  33. # light walls or not
  34. FOV_LIGHT_WALLS = True
  35. TORCH_RADIUS = 10
  36.  
  37. LIMIT_FPS = 20
  38. ROGUELIKE_NAME = 'Stewa Roguelike'
  39.  
  40.  
  41. color_dark_wall = libtcod.Color(57, 22, 10)
  42. color_light_wall = libtcod.Color(115, 45, 20)
  43. color_dark_ground = libtcod.Color(10, 55, 55)
  44. color_light_ground = libtcod.Color(20, 110, 110)
  45.  
  46.  
  47. class Tile:
  48. # a tile of the map and its properties
  49. def __init__(self, blocked, block_sight = None):
  50. self.blocked = blocked
  51.  
  52. # all tiles start unexplored
  53. self.explored = False
  54.  
  55. # by default, if a tile is blocked, it also blocks sight
  56. if block_sight is None: block_sight = blocked
  57. self.block_sight = block_sight
  58.  
  59. class Rect:
  60. # a rectangle on the map. used to characterize a room.
  61. def __init__(self, x, y, w, h):
  62. self.x1 = x
  63. self.y1 = y
  64. self.x2 = x + w
  65. self.y2 = y + h
  66.  
  67. def center(self):
  68. center_x = (self.x1 + self.x2) / 2
  69. center_y = (self.y1 + self.y2) / 2
  70. return (center_x, center_y)
  71.  
  72. def intersect(self, other):
  73. # returns true if this rectangle intersects with another one
  74. return (self.x1 <= other.x2 and self.x2 >= other.x1 and
  75. self.y1 <= other.y2 and self.y2 >= other.y1)
  76.  
  77. class Object:
  78. # this is a generic object: the player, a monster, an item, the stairs...
  79. # it's always represented by a character on screen.
  80. def __init__(self, x, y, char, name, color, blocks=False, fighter=None, ai=None):
  81. self.name = name
  82. self.blocks = blocks
  83. self.x = x
  84. self.y = y
  85. self.char = char
  86. self.color = color
  87. self.fighter = fighter
  88. if self.fighter: # let the fighter component know who owns it
  89. self.fighter.owner = self
  90.  
  91. self.ai = ai
  92. if self.ai:
  93. self.ai.owner = self
  94.  
  95.  
  96. def move(self, dx, dy):
  97. # move by the given amount, if the destination is not blocked
  98. if not is_blocked(self.x + dx, self.y + dy):
  99. self.x += dx
  100. self.y += dy
  101.  
  102. def draw(self):
  103. # only show if it's visible to the player
  104. if libtcod.map_is_in_fov(fov_map, self.x, self.y):
  105. # set the color and then draw the character that represents this object at its position
  106. libtcod.console_set_default_foreground(con, self.color)
  107. libtcod.console_put_char(con, self.x, self.y, self.char, libtcod.BKGND_NONE)
  108.  
  109. def clear(self):
  110. # erase the character that represents this object
  111. libtcod.console_put_char(con, self.x, self.y, ' ', libtcod.BKGND_NONE)
  112.  
  113. def move_towards(self, target_x, target_y):
  114. # vector from this object to the target, and distance
  115. dx = target_x - self.x
  116. dy = target_y - self.y
  117. distance = math.sqrt(dx ** 2 + dy ** 2)
  118.  
  119. # normalize it to length 1 (preserving direction), then round it and
  120. # concert to integer so the movement is restricted to the map grid
  121. dx = int(round(dx / distance))
  122. dy = int(round(dy / distance))
  123. self.move(dx, dy)
  124.  
  125. def distance_to(self, other):
  126. # return the distance to another object
  127. dx = other.x - self.x
  128. dy = other.y - self.y
  129. return math.sqrt(dx ** 2 + dy ** 2)
  130.  
  131. def send_to_back(self):
  132. # make this object to be drawn first, so all others appear above it if they're in the same tile
  133. global objects
  134. objects.remove(self)
  135. objects.insert(0, self)
  136.  
  137. class Fighter:
  138. # combat-related properties and methods (monster, player, NPC)
  139. def __init__(self, hp, defense, power, death_function=None):
  140. self.max_hp = hp
  141. self.hp = hp
  142. self.defense = defense
  143. self.power = power
  144. self.death_function = death_function
  145.  
  146. def take_damage(self, damage):
  147. # apply damage if possible
  148. if damage > 0:
  149. self.hp -= damage
  150.  
  151. # check for death. If there's a death function, call it.
  152. if self.hp <= 0:
  153. function = self.death_function
  154. if function is not None:
  155. function(self.owner)
  156.  
  157. def attack(self, target):
  158. # a simple formula for attack damage
  159. damage = self.power - target.fighter.defense
  160.  
  161. if damage > 0:
  162. # make the target take some damage
  163. message(self.owner.name.capitalize() + ' attacks ' + target.name + ' for '
  164. + str(damage) + ' hits points.')
  165. target.fighter.take_damage(damage)
  166. else:
  167. message(self.owner.name.capitalize() + ' attacks ' + target.name + ' but it has no effect!')
  168.  
  169. class BasicMonster:
  170. # AI for a basic monster.
  171. def take_turn(self):
  172. # a basic monster takes its turn. If you can see it, it can see you.
  173. monster = self.owner
  174. if libtcod.map_is_in_fov(fov_map, monster.x, monster.y):
  175. # move towards player if far away
  176. if monster.distance_to(player) >= 2:
  177. monster.move_towards(player.x, player.y)
  178.  
  179. # close enough to attack
  180. elif player.fighter.hp > 0:
  181. monster.fighter.attack(player)
  182.  
  183. def is_blocked(x,y):
  184. # first test the map tile
  185. if map[x][y].blocked:
  186. return True
  187.  
  188. # check for any blocking objects
  189. for object in objects:
  190. if object.blocks and object.x ==x and object.y ==y:
  191. return True
  192.  
  193. return False
  194.  
  195. def message(new_msg, color = libtcod.white):
  196. # split the message if necessary, among multiple lines
  197. new_msg_lines = textwrap.wrap(new_msg, MSG_WIDTH)
  198.  
  199. for line in new_msg_lines:
  200. # if the buffer is full, remove the first line to make room for the new one
  201. if len(game_msgs) == MSG_HEIGHT:
  202. del game_msgs[0]
  203.  
  204. # add the new line as a tuple, with the text and the color
  205. game_msgs.append( (line, color) )
  206.  
  207. def create_room(room):
  208. global map
  209. # go through the tiles in the rectangle and make them passable
  210. for x in range(room.x1 + 1, room.x2):
  211. for y in range(room.y1 + 1, room.y2):
  212. map[x][y].blocked = False
  213. map[x][y].block_sight = False
  214.  
  215. def place_objects(room):
  216. # choose random number of monsters
  217. num_monsters = libtcod.random_get_int(0, 0, MAX_ROOM_MONSTERS)
  218.  
  219. for i in range(num_monsters):
  220. # choose random spot for this monster
  221. x = libtcod.random_get_int(0, room.x1 + 1, room.x2 - 1)
  222. y = libtcod.random_get_int(0, room.y1 + 1, room.y2 - 1)
  223.  
  224. # if the tile isn't blocked...
  225. if not is_blocked(x, y):
  226. # chances: 55% chance of goblin, 30% chance of orc, 14% chance of troll, 1% chance of dragon
  227. choice = libtcod.random_get_int(0, 0, 100)
  228. if choice < 55:
  229. # create a goblin
  230. fighter_component = Fighter(hp=10, defense=0, power=3, death_function=monster_death)
  231. ai_component = BasicMonster()
  232.  
  233. monster = Object(x, y, 'g', 'goblin', libtcod.black, blocks=True,
  234. fighter=fighter_component, ai=ai_component)
  235. elif choice < 55+30:
  236. # create an orc
  237. fighter_component = Fighter(hp=15, defense=1, power=4, death_function=monster_death)
  238. ai_component = BasicMonster()
  239.  
  240. monster = Object(x, y, 'O', 'ogre', libtcod.dark_red, blocks=True,
  241. fighter=fighter_component, ai=ai_component)
  242. elif choice < 55+30+14:
  243. # create a troll
  244. fighter_component = Fighter(hp=20, defense=1, power=5, death_function=monster_death)
  245. ai_component = BasicMonster()
  246.  
  247. monster = Object(x, y, 'T', 'troll', libtcod.darker_orange,
  248. blocks=True, fighter=fighter_component, ai=ai_component)
  249. else:
  250. # create a dragon
  251. fighter_component = Fighter(hp=200, defense=5, power=12, death_function=monster_death)
  252. ai_component = BasicMonster()
  253.  
  254. monster = Object(x, y, 'D', 'dragon', libtcod.red, blocks=True,
  255. fighter=fighter_component, ai=ai_component)
  256.  
  257. objects.append(monster)
  258.  
  259. def create_h_tunnel(x1, x2, y):
  260. global map
  261. # horizontal tunnel. min() and max() are used in case x1>x2
  262. for x in range(min(x1, x2), max(x1, x2) + 1):
  263. map[x][y].blocked = False
  264. map[x][y].block_sight = False
  265.  
  266. def create_v_tunnel(y1, y2, x):
  267. global map
  268. # vertical tunnel
  269. for y in range(min(y1, y2), max(y1, y2) + 1):
  270. map[x][y].blocked = False
  271. map[x][y].block_sight = False
  272.  
  273. def make_map():
  274. global map, player
  275.  
  276. # fill map with "blocked" tiles
  277. map = [[ Tile(True)
  278. for y in range(MAP_HEIGHT) ]
  279. for x in range(MAP_WIDTH) ]
  280.  
  281. rooms = []
  282. num_rooms = 0
  283.  
  284. for r in range(MAX_ROOMS):
  285. # random width and height
  286. w = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
  287. h = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
  288. # random position without going out of the boundaries of the map
  289. x = libtcod.random_get_int(0, 0, MAP_WIDTH - w - 1)
  290. y = libtcod.random_get_int(0, 0, MAP_HEIGHT - h - 1)
  291.  
  292. # "Rect" class makes rectangles easier to work with
  293. new_room = Rect(x, y, w, h)
  294.  
  295. # run through the other rooms and see if they intersect with this one
  296. failed = False
  297. for other_room in rooms:
  298. if new_room.intersect(other_room):
  299. failed = True
  300. break
  301.  
  302. if not failed:
  303. # this means there are no intersections, so this room is valid
  304.  
  305. # "paint" it to the map's tiles
  306. create_room(new_room)
  307.  
  308. # center coordinates of new room, will be useful later
  309. (new_x, new_y) = new_room.center()
  310.  
  311. if num_rooms == 0:
  312. # this is the first room, where the player starts at
  313. player.x = new_x
  314. player.y = new_y
  315. else:
  316. # all rooms after the first:
  317. # connect it to the previous room with a tunnel
  318.  
  319. # center coordinates of previous room
  320. (prev_x, prev_y) = rooms[num_rooms-1].center()
  321.  
  322. # draw a coin (random number that is either 0 or 1)
  323. if libtcod.random_get_int(0, 0, 1) == 1:
  324. # first move horizontally, then vertically
  325. create_h_tunnel(prev_x, new_x, prev_y)
  326. create_v_tunnel(prev_y, new_y, new_x)
  327. else:
  328. # first move vertically, then horizontally
  329. create_v_tunnel(prev_y, new_y, prev_x)
  330. create_h_tunnel(prev_x, new_x, new_y)
  331.  
  332. # add contents to room, ie monsters
  333. place_objects(new_room)
  334.  
  335. # finally, append the new room to the list
  336. rooms.append(new_room)
  337. num_rooms += 1
  338.  
  339.  
  340. def render_all():
  341. global fov_map, color_dark_wall, color_light_wall
  342. global color_dark_ground, color_light_ground
  343. global fov_recompute
  344.  
  345. if fov_recompute:
  346. # recompute FOV if needed (the player moved or something)
  347. fov_recompute = False
  348. libtcod.map_compute_fov(fov_map, player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO)
  349.  
  350. # go through all tiles, and set their background color according to the FOV
  351. for y in range(MAP_HEIGHT):
  352. for x in range(MAP_WIDTH):
  353. visible = libtcod.map_is_in_fov(fov_map, x, y)
  354. wall = map[x][y].block_sight
  355. if not visible:
  356. # if it's not visible right now, the player can only see it if it's explored
  357. if map[x][y].explored:
  358. if wall:
  359. libtcod.console_set_char_background(con, x, y, color_dark_wall,
  360. libtcod.BKGND_SET)
  361. else:
  362. libtcod.console_set_char_background(con, x, y, color_dark_ground,
  363. libtcod.BKGND_SET)
  364. else:
  365. # it's visible
  366. if wall:
  367. libtcod.console_set_char_background(con, x, y, color_light_wall,
  368. libtcod.BKGND_SET )
  369. else:
  370. libtcod.console_set_char_background(con, x, y, color_light_ground,
  371. libtcod.BKGND_SET )
  372. # since it's visible, explore it
  373. map[x][y].explored = True
  374.  
  375. # draw all objects in the list
  376. for object in objects:
  377. if object != player:
  378. object.draw()
  379. player.draw()
  380.  
  381. # blit the contents of "con" to the root console
  382. libtcod.console_blit(con, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0)
  383.  
  384. # prepare to render the GUI panel
  385. libtcod.console_set_default_background(panel, libtcod.black)
  386. libtcod.console_clear(panel)
  387.  
  388. # print the game messages, one line at a time
  389. y = 1
  390. for (line, color) in game_msgs:
  391. libtcod.console_set_default_foreground(panel, color)
  392. libtcod.console_print_ex(panel, MSG_X, y, libtcod.BKGND_NONE, libtcod.LEFT, line)
  393. y += 1
  394.  
  395. # show the player's stats
  396. render_bar(1, 1, BAR_WIDTH, 'HP', player.fighter.hp, player.fighter.max_hp,
  397. libtcod.light_red, libtcod.darker_red)
  398.  
  399. # display names of objects under the mouse
  400. libtcod.console_set_default_foreground(panel, libtcod.light_gray)
  401. libtcod.console_print_ex(panel, 1, 0, libtcod.BKGND_NONE, libtcod.LEFT, get_names_under_mouse())
  402.  
  403. # blit the contents of the "panel" to the root console
  404. libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
  405.  
  406. def render_bar(x, y, total_width, name, value, maximum, bar_color, back_color):
  407. # render a bar (HP, xp, etc). First calculate bar width
  408. bar_width = int(float(value) / maximum * total_width)
  409.  
  410. # render the background first
  411. libtcod.console_set_default_background(panel, back_color)
  412. libtcod.console_rect(panel, x, y, total_width, 1, False, libtcod.BKGND_SCREEN)
  413.  
  414. # now render the bar on top
  415. libtcod.console_set_default_background(panel, bar_color)
  416. if bar_width > 0:
  417. libtcod.console_rect(panel, x, y, bar_width, 1, False, libtcod.BKGND_SCREEN)
  418.  
  419. # finally, come centered text with the values
  420. libtcod.console_set_default_foreground(panel, libtcod.white)
  421. libtcod.console_print_ex(panel, x + total_width / 2, y, libtcod.BKGND_NONE,
  422. libtcod.CENTER, name + ' : ' + str(value) + '/' + str(maximum))
  423.  
  424. def player_move_or_attack(dx, dy):
  425. global fov_recompute
  426.  
  427. # the coordinates the player is moving to/attacking
  428. x = player.x + dx
  429. y = player.y + dy
  430.  
  431. # try to find an attackable object there
  432. target = None
  433. for object in objects:
  434. if object.fighter and object.x == x and object.y == y:
  435. target = object
  436. break
  437.  
  438. if target is not None:
  439. player.fighter.attack(target)
  440. else:
  441. player.move(dx, dy)
  442. fov_recompute = True
  443.  
  444. def player_death(player):
  445. # the game ended!
  446. global game_state
  447. message('You died!', libtcod.red)
  448. game_state = 'dead'
  449.  
  450. # for added effect, transform the player into a corpse!
  451. player.char = '%'
  452. player.color = libtcod.dark_red
  453.  
  454. def monster_death(monster):
  455. # transform it into a nasty corpse! it doesn't block, can't be attacked,
  456. # doesn't move
  457. message(monster.name.capitalize() + ' is dead!', libtcod.orange)
  458. monster.char = '%'
  459. monster.color = libtcod.dark_red
  460. monster.blocks = False
  461. monster.fighter = None
  462. monster.ai = None
  463. monster.name = 'remains of ' + monster.name
  464. monster.send_to_back()
  465.  
  466. def get_names_under_mouse():
  467. global mouse
  468.  
  469. # return a string with the names of all objects under the mosue
  470. (x, y) = (mouse.cx, mouse.cy)
  471.  
  472. # create a list with the names of all objects at the mouse's coordinates and in FOV
  473. names = [obj.name for obj in objects if obj.x == x and obj.y == y and
  474. libtcod.map_is_in_fov(fov_map, obj.x, obj.y)]
  475.  
  476. names = ', '.join(names) # join the names, separated by commas
  477. return names.capitalize()
  478.  
  479. def handle_keys():
  480. global key
  481. # key = libtcod.console_wait_for_keypress(True)
  482.  
  483. if key.vk == libtcod.KEY_ENTER and key.lalt:
  484. # Alt+Enter: toggle fullscreen
  485. libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())
  486.  
  487. elif key.vk == libtcod.KEY_ESCAPE:
  488. return 'exit' # exit game
  489.  
  490. # movement keys
  491. if game_state == 'playing':
  492. if key.vk == libtcod.KEY_UP and key.pressed == True:
  493. player_move_or_attack(0, -1)
  494. print 'up'
  495. print key.vk
  496. print key.c
  497. print key.pressed
  498.  
  499. elif key.vk == libtcod.KEY_DOWN:
  500. player_move_or_attack(0, 1)
  501. print 'down'
  502. print key.vk
  503. print key.c
  504. print key.pressed
  505.  
  506. elif key.vk == libtcod.KEY_LEFT:
  507. player_move_or_attack(-1, 0)
  508. print 'left'
  509. print key.vk
  510. print key.c
  511. print key.pressed
  512.  
  513. elif key.vk == libtcod.KEY_RIGHT:
  514. player_move_or_attack(1, 0)
  515. print 'right'
  516. print key.vk
  517. print key.c
  518. print key.pressed
  519.  
  520. else:
  521. return 'didnt-take-turn'
  522.  
  523.  
  524. #############################################
  525. # Initialization & Main Loop
  526. #############################################
  527.  
  528. libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)
  529. libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, ROGUELIKE_NAME, False)
  530. libtcod.sys_set_fps(LIMIT_FPS)
  531. con = libtcod.console_new(MAP_WIDTH, MAP_HEIGHT)
  532. panel = libtcod.console_new(SCREEN_WIDTH, PANEL_HEIGHT)
  533.  
  534. # create object representing the player
  535. fighter_component = Fighter(hp=30, defense=2, power=5, death_function=player_death)
  536. player = Object(0, 0, '@', 'player', libtcod.white, blocks=True, fighter=fighter_component)
  537.  
  538. # the list of objects starting with the player
  539. objects = [player]
  540.  
  541. # generate map (at this point it's not drawn to the screen)
  542. make_map()
  543.  
  544. # create the FOV map, according to the generated map
  545. fov_map = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT)
  546. for y in range(MAP_HEIGHT):
  547. for x in range(MAP_WIDTH):
  548. libtcod.map_set_properties(fov_map, x, y, not map[x][y].block_sight, not map[x][y].blocked)
  549.  
  550.  
  551. fov_recompute = True
  552. game_state = 'playing'
  553. player_action = None
  554.  
  555. # create the list of game mesages and their colors. Starts empty.
  556. game_msgs = []
  557.  
  558. # initial message
  559. message("""Welcome stranger! Prepare to perish in the dungeons of STEWA.
  560. Beware - sometimes, Dragons roam.""", libtcod.red)
  561.  
  562. mouse = libtcod.Mouse()
  563. key = libtcod.Key()
  564.  
  565. while not libtcod.console_is_window_closed():
  566. # render the screen
  567. libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS|libtcod.EVENT_MOUSE,key,mouse)
  568. render_all()
  569.  
  570. libtcod.console_flush()
  571.  
  572. # erase all objects at their old locations, before they move
  573. for object in objects:
  574. object.clear()
  575.  
  576. # handle keys and exit game if needed
  577. player_action = handle_keys()
  578. if player_action == 'exit':
  579. break
  580.  
  581. # let monsters take their turn
  582. if game_state == 'playing' and player_action != 'didnt-take-turn':
  583. for object in objects:
  584. if object.ai:
  585. object.ai.take_turn()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement