#!/usr/bin/env python """This code demonstrates various usages of python-tcod.""" # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights to these samples. # https://creativecommons.org/publicdomain/zero/1.0/ from __future__ import annotations import copy import importlib.metadata import math import random import sys import time import warnings from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any import numpy as np import tcod.bsp import tcod.cffi import tcod.console import tcod.constants import tcod.context import tcod.event import tcod.image import tcod.los import tcod.map import tcod.noise import tcod.path import tcod.render import tcod.sdl.mouse import tcod.sdl.render import tcod.tileset from tcod import libtcodpy from tcod.event import KeySym if TYPE_CHECKING: from numpy.typing import NDArray DATA_DIR = Path(__file__).parent / "../libtcod/data" """Path of the samples data directory.""" assert DATA_DIR.exists(), "Data directory is missing, did you forget to run `git submodule update --init`?" WHITE = (255, 255, 255) GREY = (127, 127, 127) BLACK = (0, 0, 0) LIGHT_BLUE = (63, 63, 255) LIGHT_YELLOW = (255, 255, 63) SAMPLE_SCREEN_WIDTH = 46 SAMPLE_SCREEN_HEIGHT = 20 SAMPLE_SCREEN_X = 20 SAMPLE_SCREEN_Y = 10 FONT = DATA_DIR / "fonts/dejavu10x10_gs_tc.png" # Mutable global names. context: tcod.context.Context tileset: tcod.tileset.Tileset console_render: tcod.render.SDLConsoleRender # Optional SDL renderer. sample_minimap: tcod.sdl.render.Texture # Optional minimap texture. root_console = tcod.console.Console(80, 50) sample_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) cur_sample = 0 # Current selected sample. frame_times = [time.perf_counter()] frame_length = [0.0] START_TIME = time.perf_counter() def _get_elapsed_time() -> float: """Return time passed since the start of the program.""" return time.perf_counter() - START_TIME class Sample: """Samples base class.""" name: str = "???" def on_enter(self) -> None: """Called when entering a sample.""" def on_draw(self) -> None: """Called every frame.""" def on_event(self, event: tcod.event.Event) -> None: """Called for each event.""" global cur_sample match event: case tcod.event.Quit() | tcod.event.KeyDown(sym=KeySym.ESCAPE): raise SystemExit case tcod.event.KeyDown(sym=KeySym.DOWN): cur_sample = (cur_sample + 1) % len(SAMPLES) SAMPLES[cur_sample].on_enter() draw_samples_menu() case tcod.event.KeyDown(sym=KeySym.UP): cur_sample = (cur_sample - 1) % len(SAMPLES) SAMPLES[cur_sample].on_enter() draw_samples_menu() case tcod.event.KeyDown(sym=KeySym.RETURN, mod=mod) if mod & tcod.event.Modifier.ALT: sdl_window = context.sdl_window if sdl_window: sdl_window.fullscreen = not sdl_window.fullscreen case tcod.event.KeyDown(sym=tcod.event.KeySym.PRINTSCREEN | tcod.event.KeySym.P): print("screenshot") if event.mod & tcod.event.Modifier.ALT: libtcodpy.console_save_apf(root_console, "samples.apf") print("apf") else: libtcodpy.sys_save_screenshot() print("png") case tcod.event.KeyDown(sym=sym) if sym in RENDERER_KEYS: init_context(RENDERER_KEYS[sym]) # Swap the active context for one with a different renderer class TrueColorSample(Sample): """Simple performance benchmark.""" name = "True colors" def __init__(self) -> None: """Initialize random generators.""" self.noise = tcod.noise.Noise(2, tcod.noise.Algorithm.SIMPLEX) """Noise for generating color.""" self.generator = np.random.default_rng() """Numpy generator for random text.""" def on_draw(self) -> None: """Draw this sample.""" self.interpolate_corner_colors() self.darken_background_characters() self.randomize_sample_console() sample_console.print( x=1, y=5, width=sample_console.width - 2, height=sample_console.height - 1, text="The Doryen library uses 24 bits colors, for both background and foreground.", fg=WHITE, bg=GREY, bg_blend=libtcodpy.BKGND_MULTIPLY, alignment=libtcodpy.CENTER, ) def get_corner_colors(self) -> NDArray[np.uint8]: """Return 4 random 8-bit colors, smoothed over time.""" noise_samples_ij = ( [ # i coordinates are per color channel per color [0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], ], time.perf_counter(), # j coordinate is time broadcast to all samples ) colors = self.noise[noise_samples_ij] colors = ((colors + 1.0) * (0.5 * 255.0)).clip(min=0, max=255) # Convert -1..1 to 0..255 return colors.astype(np.uint8) def interpolate_corner_colors(self) -> None: """Interpolate corner colors across the sample console.""" colors = self.get_corner_colors() top = np.linspace(colors[0], colors[1], SAMPLE_SCREEN_WIDTH) bottom = np.linspace(colors[2], colors[3], SAMPLE_SCREEN_WIDTH) sample_console.bg[:] = np.linspace(top, bottom, SAMPLE_SCREEN_HEIGHT) def darken_background_characters(self) -> None: """Darken background characters.""" sample_console.fg[:] = sample_console.bg[:] sample_console.fg[:] //= 2 def randomize_sample_console(self) -> None: """Randomize sample console characters.""" sample_console.ch[:] = self.generator.integers( low=ord("a"), high=ord("z"), endpoint=True, size=sample_console.ch.size, dtype=np.intc, ).reshape(sample_console.ch.shape) class OffscreenConsoleSample(Sample): """Console blit example.""" name = "Offscreen console" CONSOLE_MOVE_RATE = 1 / 2 CONSOLE_MOVE_MARGIN = 5 def __init__(self) -> None: """Initialize the offscreen console.""" self.secondary = tcod.console.Console(sample_console.width // 2, sample_console.height // 2) self.screenshot = tcod.console.Console(sample_console.width, sample_console.height) self.counter = 0.0 self.x = 0 self.y = 0 self.x_dir = 1 self.y_dir = 1 self.secondary.draw_frame(0, 0, self.secondary.width, self.secondary.height, clear=False, fg=WHITE, bg=BLACK) self.secondary.print( 0, 0, width=self.secondary.width, height=self.secondary.height, text=" Offscreen console ", fg=BLACK, bg=WHITE, alignment=tcod.constants.CENTER, ) self.secondary.print( x=1, y=2, width=sample_console.width // 2 - 2, height=sample_console.height // 2, text="You can render to an offscreen console and blit in on another one, simulating alpha transparency.", fg=WHITE, bg=None, alignment=libtcodpy.CENTER, ) def on_enter(self) -> None: """Capture the previous sample screen as this samples background.""" self.counter = _get_elapsed_time() sample_console.blit(dest=self.screenshot) # get a "screenshot" of the current sample screen def on_draw(self) -> None: """Draw and animate the offscreen console.""" if _get_elapsed_time() - self.counter >= self.CONSOLE_MOVE_RATE: self.counter = _get_elapsed_time() self.x += self.x_dir self.y += self.y_dir if self.x == sample_console.width / 2 + self.CONSOLE_MOVE_MARGIN: self.x_dir = -1 elif self.x == -self.CONSOLE_MOVE_MARGIN: self.x_dir = 1 if self.y == sample_console.height / 2 + self.CONSOLE_MOVE_MARGIN: self.y_dir = -1 elif self.y == -self.CONSOLE_MOVE_MARGIN: self.y_dir = 1 self.screenshot.blit(sample_console) self.secondary.blit( sample_console, self.x, self.y, 0, 0, sample_console.width // 2, sample_console.height // 2, 1.0, 0.75 ) class LineDrawingSample(Sample): name = "Line drawing" FLAG_NAMES = ( "BKGND_NONE", "BKGND_SET", "BKGND_MULTIPLY", "BKGND_LIGHTEN", "BKGND_DARKEN", "BKGND_SCREEN", "BKGND_COLOR_DODGE", "BKGND_COLOR_BURN", "BKGND_ADD", "BKGND_ADDALPHA", "BKGND_BURN", "BKGND_OVERLAY", "BKGND_ALPHA", ) def __init__(self) -> None: self.mk_flag = libtcodpy.BKGND_SET self.bk_flag = libtcodpy.BKGND_SET self.background = tcod.console.Console(sample_console.width, sample_console.height) # initialize the colored background self.background.bg[:, :, 0] = np.linspace(0, 255, self.background.width) self.background.bg[:, :, 2] = np.linspace(0, 255, self.background.height)[:, np.newaxis] self.background.bg[:, :, 1] = (self.background.bg[:, :, 0].astype(int) + self.background.bg[:, :, 2]) / 2 self.background.ch[:] = ord(" ") def on_event(self, event: tcod.event.Event) -> None: match event: case tcod.event.KeyDown(sym=KeySym.RETURN | KeySym.KP_ENTER): self.bk_flag += 1 if (self.bk_flag & 0xFF) > libtcodpy.BKGND_ALPH: self.bk_flag = libtcodpy.BKGND_NONE case _: super().on_event(event) def on_draw(self) -> None: alpha = 0.0 if (self.bk_flag & 0xFF) == libtcodpy.BKGND_ALPH: # for the alpha mode, update alpha every frame alpha = (1.0 + math.cos(time.time() * 2)) / 2.0 self.bk_flag = libtcodpy.BKGND_ALPHA(int(alpha)) elif (self.bk_flag & 0xFF) == libtcodpy.BKGND_ADDA: # for the add alpha mode, update alpha every frame alpha = (1.0 + math.cos(time.time() * 2)) / 2.0 self.bk_flag = libtcodpy.BKGND_ADDALPHA(int(alpha)) self.background.blit(sample_console) rect_y = int((sample_console.height - 2) * ((1.0 + math.cos(time.time())) / 2.0)) for x in range(sample_console.width): value = x * 255 // sample_console.width col = (value, value, value) sample_console.draw_rect(x=x, y=rect_y, width=1, height=3, ch=0, fg=None, bg=col, bg_blend=self.bk_flag) angle = time.time() * 2.0 cos_angle = math.cos(angle) sin_angle = math.sin(angle) xo = int(sample_console.width // 2 * (1 + cos_angle)) yo = int(sample_console.height // 2 + sin_angle * sample_console.width // 2) xd = int(sample_console.width // 2 * (1 - cos_angle)) yd = int(sample_console.height // 2 - sin_angle * sample_console.width // 2) # draw the line # in python the easiest way is to use the line iterator for x, y in tcod.los.bresenham((xo, yo), (xd, yd)).tolist(): if 0 <= x < sample_console.width and 0 <= y < sample_console.height: sample_console.draw_rect(x, y, width=1, height=1, ch=0, fg=None, bg=LIGHT_BLUE, bg_blend=self.bk_flag) sample_console.print(2, 2, f"{self.FLAG_NAMES[self.bk_flag & 0xFF]} (ENTER to change)", fg=WHITE, bg=None) class NoiseSample(Sample): name = "Noise" NOISE_OPTIONS = ( # (name, algorithm, implementation) ( "perlin noise", tcod.noise.Algorithm.PERLIN, tcod.noise.Implementation.SIMPLE, ), ( "simplex noise", tcod.noise.Algorithm.SIMPLEX, tcod.noise.Implementation.SIMPLE, ), ( "wavelet noise", tcod.noise.Algorithm.WAVELET, tcod.noise.Implementation.SIMPLE, ), ( "perlin fbm", tcod.noise.Algorithm.PERLIN, tcod.noise.Implementation.FBM, ), ( "perlin turbulence", tcod.noise.Algorithm.PERLIN, tcod.noise.Implementation.TURBULENCE, ), ( "simplex fbm", tcod.noise.Algorithm.SIMPLEX, tcod.noise.Implementation.FBM, ), ( "simplex turbulence", tcod.noise.Algorithm.SIMPLEX, tcod.noise.Implementation.TURBULENCE, ), ( "wavelet fbm", tcod.noise.Algorithm.WAVELET, tcod.noise.Implementation.FBM, ), ( "wavelet turbulence", tcod.noise.Algorithm.WAVELET, tcod.noise.Implementation.TURBULENCE, ), ) def __init__(self) -> None: self.func = 0 self.dx = 0.0 self.dy = 0.0 self.octaves = 4.0 self.zoom = 3.0 self.hurst = libtcodpy.NOISE_DEFAULT_HURST self.lacunarity = libtcodpy.NOISE_DEFAULT_LACUNARITY self.noise = self.get_noise() self.img = tcod.image.Image(SAMPLE_SCREEN_WIDTH * 2, SAMPLE_SCREEN_HEIGHT * 2) @property def algorithm(self) -> int: return self.NOISE_OPTIONS[self.func][1] @property def implementation(self) -> int: return self.NOISE_OPTIONS[self.func][2] def get_noise(self) -> tcod.noise.Noise: return tcod.noise.Noise( 2, self.algorithm, self.implementation, self.hurst, self.lacunarity, self.octaves, seed=None, ) def on_draw(self) -> None: self.dx = _get_elapsed_time() * 0.25 self.dy = _get_elapsed_time() * 0.25 for y in range(2 * sample_console.height): for x in range(2 * sample_console.width): f = [ self.zoom * x / (2 * sample_console.width) + self.dx, self.zoom * y / (2 * sample_console.height) + self.dy, ] value = self.noise.get_point(*f) c = int((value + 1.0) / 2.0 * 255) c = max(0, min(c, 255)) self.img.put_pixel(x, y, (c // 2, c // 2, c)) rect_w = 24 rect_h = 13 if self.implementation == tcod.noise.Implementation.SIMPLE: rect_h = 10 sample_console.draw_semigraphics(np.asarray(self.img)) sample_console.draw_rect( 2, 2, rect_w, rect_h, ch=0, fg=None, bg=GREY, bg_blend=libtcodpy.BKGND_MULTIPLY, ) sample_console.fg[2 : 2 + rect_h, 2 : 2 + rect_w] = ( sample_console.fg[2 : 2 + rect_h, 2 : 2 + rect_w] * GREY / 255 ) for cur_func in range(len(self.NOISE_OPTIONS)): text = f"{cur_func + 1} : {self.NOISE_OPTIONS[cur_func][0]}" if cur_func == self.func: sample_console.print(2, 2 + cur_func, text, fg=WHITE, bg=LIGHT_BLUE) else: sample_console.print(2, 2 + cur_func, text, fg=GREY, bg=None) sample_console.print(2, 11, f"Y/H : zoom ({self.zoom:2.1f})", fg=WHITE, bg=None) if self.implementation != tcod.noise.Implementation.SIMPLE: sample_console.print( 2, 12, f"E/D : hurst ({self.hurst:2.1f})", fg=WHITE, bg=None, ) sample_console.print( 2, 13, f"R/F : lacunarity ({self.lacunarity:2.1f})", fg=WHITE, bg=None, ) sample_console.print( 2, 14, f"T/G : octaves ({self.octaves:2.1f})", fg=WHITE, bg=None, ) def on_event(self, event: tcod.event.Event) -> None: match event: case tcod.event.KeyDown(sym=sym) if KeySym.N9 >= sym >= KeySym.N1: self.func = sym - tcod.event.KeySym.N1 self.noise = self.get_noise() case tcod.event.KeyDown(sym=KeySym.E): self.hurst += 0.1 self.noise = self.get_noise() case tcod.event.KeyDown(sym=KeySym.D): self.hurst -= 0.1 self.noise = self.get_noise() case tcod.event.KeyDown(sym=KeySym.R): self.lacunarity += 0.5 self.noise = self.get_noise() case tcod.event.KeyDown(sym=KeySym.F): self.lacunarity -= 0.5 self.noise = self.get_noise() case tcod.event.KeyDown(sym=KeySym.T): self.octaves += 0.5 self.noise.octaves = self.octaves case tcod.event.KeyDown(sym=KeySym.G): self.octaves -= 0.5 self.noise.octaves = self.octaves case tcod.event.KeyDown(sym=KeySym.Y): self.zoom += 0.2 case tcod.event.KeyDown(sym=KeySym.H): self.zoom -= 0.2 case _: super().on_event(event) ############################################# # field of view sample ############################################# DARK_WALL = (0, 0, 100) LIGHT_WALL = (130, 110, 50) DARK_GROUND = (50, 50, 150) LIGHT_GROUND = (200, 180, 50) SAMPLE_MAP_ = ( "##############################################", "####################### #################", "##################### # ###############", "###################### ### ###########", "################## ##### ####", "################ ######## ###### ####", "############### #################### ####", "################ ###### ##", "######## ####### ###### # # # ##", "######## ###### ### ##", "######## ##", "#### ###### ### # # # ##", "#### ### ########## #### ##", "#### ### ########## ###########=##########", "#### ################## ##### #####", "#### ### #### ##### #####", "#### # #### #####", "######## # #### ##### #####", "######## ##### ####################", "##############################################", ) SAMPLE_MAP: NDArray[Any] = np.array([[ord(c) for c in line] for line in SAMPLE_MAP_]) FOV_ALGO_NAMES = ( "BASIC ", "DIAMOND ", "SHADOW ", "PERMISSIVE0", "PERMISSIVE1", "PERMISSIVE2", "PERMISSIVE3", "PERMISSIVE4", "PERMISSIVE5", "PERMISSIVE6", "PERMISSIVE7", "PERMISSIVE8", "RESTRICTIVE", "SYMMETRIC_SHADOWCAST", ) TORCH_RADIUS = 10 SQUARED_TORCH_RADIUS = TORCH_RADIUS * TORCH_RADIUS class FOVSample(Sample): name = "Field of view" def __init__(self) -> None: self.player_x = 20 self.player_y = 10 self.torch = False self.light_walls = True self.algo_num = libtcodpy.FOV_SYMMETRIC_SHADOWCAST self.noise = tcod.noise.Noise(1) # 1D noise for the torch flickering. map_shape = (SAMPLE_SCREEN_HEIGHT, SAMPLE_SCREEN_WIDTH) self.walkable: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool) self.walkable[:] = SAMPLE_MAP[:] == ord(" ") self.transparent: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool) self.transparent[:] = self.walkable[:] | (SAMPLE_MAP[:] == ord("=")) # Lit background colors for the map. self.light_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, LIGHT_GROUND, dtype="3B") self.light_map_bg[SAMPLE_MAP[:] == ord("#")] = LIGHT_WALL # Dark background colors for the map. self.dark_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, DARK_GROUND, dtype="3B") self.dark_map_bg[SAMPLE_MAP[:] == ord("#")] = DARK_WALL def draw_ui(self) -> None: sample_console.print( 1, 1, "IJKL : move around\nT : torch fx {}\nW : light walls {}\n+-: algo {}".format( "on " if self.torch else "off", "on " if self.light_walls else "off", FOV_ALGO_NAMES[self.algo_num], ), fg=WHITE, bg=None, ) def on_draw(self) -> None: sample_console.clear() # Draw the help text & player @. self.draw_ui() sample_console.print(self.player_x, self.player_y, "@") # Draw windows. sample_console.rgb["ch"][SAMPLE_MAP[:] == ord("=")] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL sample_console.rgb["fg"][SAMPLE_MAP[:] == ord("=")] = BLACK # Get a 2D boolean array of visible cells. fov = tcod.map.compute_fov( transparency=self.transparent, pov=(self.player_y, self.player_x), radius=TORCH_RADIUS if self.torch else 0, light_walls=self.light_walls, algorithm=self.algo_num, ) if self.torch: # Derive the touch from noise based on the current time. torch_t = _get_elapsed_time() * 5 # Randomize the light position between -1.5 and 1.5 torch_x = self.player_x + self.noise.get_point(torch_t) * 1.5 torch_y = self.player_y + self.noise.get_point(torch_t + 11) * 1.5 # Extra light brightness. brightness = 0.2 * self.noise.get_point(torch_t + 17) # Get the squared distance using a mesh grid. y, x = np.mgrid[:SAMPLE_SCREEN_HEIGHT, :SAMPLE_SCREEN_WIDTH] # Center the mesh grid on the torch position. x = x.astype(np.float32) - torch_x y = y.astype(np.float32) - torch_y distance_squared = x**2 + y**2 # 2D squared distance array. # Get the currently visible cells. visible = (distance_squared < SQUARED_TORCH_RADIUS) & fov # Invert the values, so that the center is the 'brightest' point. light = SQUARED_TORCH_RADIUS - distance_squared light /= SQUARED_TORCH_RADIUS # Convert into non-squared distance. light += brightness # Add random brightness. light.clip(0, 1, out=light) # Clamp values in-place. light[~visible] = 0 # Set non-visible areas to darkness. # Setup background colors for floating point math. light_bg: NDArray[np.float16] = self.light_map_bg.astype(np.float16) dark_bg: NDArray[np.float16] = self.dark_map_bg.astype(np.float16) # Linear interpolation between colors. sample_console.rgb["bg"] = dark_bg + (light_bg - dark_bg) * light[..., np.newaxis] else: sample_console.bg[...] = np.select( condlist=[fov[:, :, np.newaxis]], choicelist=[self.light_map_bg], default=self.dark_map_bg, ) def on_event(self, event: tcod.event.Event) -> None: MOVE_KEYS = { # noqa: N806 tcod.event.KeySym.I: (0, -1), tcod.event.KeySym.J: (-1, 0), tcod.event.KeySym.K: (0, 1), tcod.event.KeySym.L: (1, 0), } FOV_SELECT_KEYS = { # noqa: N806 tcod.event.KeySym.MINUS: -1, tcod.event.KeySym.EQUALS: 1, tcod.event.KeySym.KP_MINUS: -1, tcod.event.KeySym.KP_PLUS: 1, } match event: case tcod.event.KeyDown(sym=sym) if sym in MOVE_KEYS: x, y = MOVE_KEYS[sym] if self.walkable[self.player_y + y, self.player_x + x]: self.player_x += x self.player_y += y case tcod.event.KeyDown(sym=KeySym.T): self.torch = not self.torch case tcod.event.KeyDown(sym=KeySym.W): self.light_walls = not self.light_walls case tcod.event.KeyDown(sym=sym) if sym in FOV_SELECT_KEYS: self.algo_num += FOV_SELECT_KEYS[sym] self.algo_num %= len(FOV_ALGO_NAMES) case _: super().on_event(event) class PathfindingSample(Sample): name = "Path finding" def __init__(self) -> None: """Initialize this sample.""" self.player_x = 20 self.player_y = 10 self.dest_x = 24 self.dest_y = 1 self.using_astar = True self.busy = 0.0 self.cost = SAMPLE_MAP[:] == ord(" ") self.graph = tcod.path.SimpleGraph(cost=self.cost, cardinal=70, diagonal=99) self.pathfinder = tcod.path.Pathfinder(graph=self.graph) self.background_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) # draw the dungeon self.background_console.rgb["fg"] = BLACK self.background_console.rgb["bg"] = DARK_GROUND self.background_console.rgb["bg"][SAMPLE_MAP[:] == ord("#")] = DARK_WALL self.background_console.rgb["ch"][SAMPLE_MAP[:] == ord("=")] = ord("═") def on_enter(self) -> None: """Do nothing.""" def on_draw(self) -> None: """Recompute and render pathfinding.""" self.pathfinder = tcod.path.Pathfinder(graph=self.graph) # self.pathfinder.clear() # Known issues, needs fixing # noqa: ERA001 self.pathfinder.add_root((self.player_y, self.player_x)) # draw the dungeon self.background_console.blit(dest=sample_console) sample_console.print(self.dest_x, self.dest_y, "+", fg=WHITE) sample_console.print(self.player_x, self.player_y, "@", fg=WHITE) sample_console.print(1, 1, "IJKL / mouse :\nmove destination\nTAB : A*/dijkstra", fg=WHITE, bg=None) sample_console.print(1, 4, "Using : A*", fg=WHITE, bg=None) if not self.using_astar: self.pathfinder.resolve(goal=None) reachable = self.pathfinder.distance != np.iinfo(self.pathfinder.distance.dtype).max # draw distance from player dijkstra_max_dist = float(self.pathfinder.distance[reachable].max()) np.array(self.pathfinder.distance, copy=True, dtype=np.float32) interpolate = self.pathfinder.distance[reachable] * 0.9 / dijkstra_max_dist color_delta = (np.array(DARK_GROUND) - np.array(LIGHT_GROUND)).astype(np.float32) sample_console.rgb["bg"][reachable] = np.array(LIGHT_GROUND) + interpolate[:, np.newaxis] * color_delta # draw the path path = self.pathfinder.path_to((self.dest_y, self.dest_x))[1:] sample_console.rgb["bg"][tuple(path.T)] = LIGHT_GROUND # move the creature self.busy -= frame_length[-1] if self.busy <= 0.0: self.busy = 0.2 if len(path): self.player_y = int(path.item(0, 0)) self.player_x = int(path.item(0, 1)) def on_event(self, event: tcod.event.Event) -> None: """Handle movement and UI.""" match event: case tcod.event.KeyDown(sym=KeySym.I) if self.dest_y > 0: # destination move north self.dest_y -= 1 case tcod.event.KeyDown(sym=KeySym.K) if self.dest_y < SAMPLE_SCREEN_HEIGHT - 1: # destination move south self.dest_y += 1 case tcod.event.KeyDown(sym=KeySym.J) if self.dest_x > 0: # destination move west self.dest_x -= 1 case tcod.event.KeyDown(sym=KeySym.L) if self.dest_x < SAMPLE_SCREEN_WIDTH - 1: # destination move east self.dest_x += 1 case tcod.event.KeyDown(sym=KeySym.TAB): self.using_astar = not self.using_astar case tcod.event.MouseMotion(): # Move destination via mouseover mx = event.tile.x - SAMPLE_SCREEN_X my = event.tile.y - SAMPLE_SCREEN_Y if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT: self.dest_x = int(mx) self.dest_y = int(my) case _: super().on_event(event) ############################################# # bsp sample ############################################# # draw a vertical line def vline(map_: NDArray[np.bool_], x: int, y1: int, y2: int) -> None: if y1 > y2: y1, y2 = y2, y1 for y in range(y1, y2 + 1): map_[y, x] = True # draw a vertical line up until we reach an empty space def vline_up(map_: NDArray[np.bool_], x: int, y: int) -> None: while y >= 0 and not map_[y, x]: map_[y, x] = True y -= 1 # draw a vertical line down until we reach an empty space def vline_down(map_: NDArray[np.bool_], x: int, y: int) -> None: while y < SAMPLE_SCREEN_HEIGHT and not map_[y, x]: map_[y, x] = True y += 1 # draw a horizontal line def hline(map_: NDArray[np.bool_], x1: int, y: int, x2: int) -> None: if x1 > x2: x1, x2 = x2, x1 for x in range(x1, x2 + 1): map_[y, x] = True # draw a horizontal line left until we reach an empty space def hline_left(map_: NDArray[np.bool_], x: int, y: int) -> None: while x >= 0 and not map_[y, x]: map_[y, x] = True x -= 1 # draw a horizontal line right until we reach an empty space def hline_right(map_: NDArray[np.bool_], x: int, y: int) -> None: while x < SAMPLE_SCREEN_WIDTH and not map_[y, x]: map_[y, x] = True x += 1 # the class building the dungeon from the bsp nodes def traverse_node( bsp_map: NDArray[np.bool_], node: tcod.bsp.BSP, *, bsp_min_room_size: int, bsp_random_room: bool, bsp_room_walls: bool, ) -> None: if not node.children: # calculate the room size if bsp_room_walls: node.width -= 1 node.height -= 1 if bsp_random_room: new_width = random.randint(min(node.width, bsp_min_room_size), node.width) new_height = random.randint(min(node.height, bsp_min_room_size), node.height) node.x += random.randint(0, node.width - new_width) node.y += random.randint(0, node.height - new_height) node.width, node.height = new_width, new_height # dig the room bsp_map[node.y : node.y + node.height, node.x : node.x + node.width] = True else: # resize the node to fit its sons left, right = node.children node.x = min(left.x, right.x) node.y = min(left.y, right.y) node.width = max(left.x + left.width, right.x + right.width) - node.x node.height = max(left.y + left.height, right.y + right.height) - node.y # create a corridor between the two lower nodes if node.horizontal: # vertical corridor if left.x + left.width - 1 < right.x or right.x + right.width - 1 < left.x: # no overlapping zone. we need a Z shaped corridor x1 = random.randint(left.x, left.x + left.width - 1) x2 = random.randint(right.x, right.x + right.width - 1) y = random.randint(left.y + left.height, right.y) vline_up(bsp_map, x1, y - 1) hline(bsp_map, x1, y, x2) vline_down(bsp_map, x2, y + 1) else: # straight vertical corridor min_x = max(left.x, right.x) max_x = min(left.x + left.width - 1, right.x + right.width - 1) x = random.randint(min_x, max_x) vline_down(bsp_map, x, right.y) vline_up(bsp_map, x, right.y - 1) elif left.y + left.height - 1 < right.y or right.y + right.height - 1 < left.y: # horizontal corridor # no overlapping zone. we need a Z shaped corridor y1 = random.randint(left.y, left.y + left.height - 1) y2 = random.randint(right.y, right.y + right.height - 1) x = random.randint(left.x + left.width, right.x) hline_left(bsp_map, x - 1, y1) vline(bsp_map, x, y1, y2) hline_right(bsp_map, x + 1, y2) else: # straight horizontal corridor min_y = max(left.y, right.y) max_y = min(left.y + left.height - 1, right.y + right.height - 1) y = random.randint(min_y, max_y) hline_left(bsp_map, right.x - 1, y) hline_right(bsp_map, right.x, y) class BSPSample(Sample): name = "Bsp toolkit" def __init__(self) -> None: self.bsp = tcod.bsp.BSP(1, 1, SAMPLE_SCREEN_WIDTH - 1, SAMPLE_SCREEN_HEIGHT - 1) self.bsp_map: NDArray[np.bool_] = np.zeros((SAMPLE_SCREEN_HEIGHT, SAMPLE_SCREEN_WIDTH), dtype=bool) self.bsp_depth = 8 self.bsp_min_room_size = 4 self.bsp_random_room = False # a room fills a random part of the node or the maximum available space ? self.bsp_room_walls = True # if true, there is always a wall on north & west side of a room self.bsp_generate() def bsp_generate(self) -> None: self.bsp.children = () if self.bsp_room_walls: self.bsp.split_recursive( self.bsp_depth, self.bsp_min_room_size + 1, self.bsp_min_room_size + 1, 1.5, 1.5, ) else: self.bsp.split_recursive(self.bsp_depth, self.bsp_min_room_size, self.bsp_min_room_size, 1.5, 1.5) self.bsp_refresh() def bsp_refresh(self) -> None: self.bsp_map[...] = False for node in copy.deepcopy(self.bsp).inverted_level_order(): traverse_node( self.bsp_map, node, bsp_min_room_size=self.bsp_min_room_size, bsp_random_room=self.bsp_random_room, bsp_room_walls=self.bsp_room_walls, ) def on_draw(self) -> None: sample_console.clear() rooms = "OFF" if self.bsp_random_room: rooms = "ON" sample_console.print( 1, 1, "ENTER : rebuild bsp\n" "SPACE : rebuild dungeon\n" f"+-: bsp depth {self.bsp_depth}\n" f"*/: room size {self.bsp_min_room_size}\n" f"1 : random room size {rooms}", fg=WHITE, bg=None, ) if self.bsp_random_room: walls = "OFF" if self.bsp_room_walls: walls = "ON" sample_console.print(1, 6, f"2 : room walls {walls}", fg=WHITE, bg=None) # render the level for y in range(SAMPLE_SCREEN_HEIGHT): for x in range(SAMPLE_SCREEN_WIDTH): color = DARK_GROUND if self.bsp_map[y, x] else DARK_WALL libtcodpy.console_set_char_background(sample_console, x, y, color, libtcodpy.BKGND_SET) def on_event(self, event: tcod.event.Event) -> None: match event: case tcod.event.KeyDown(sym=KeySym.RETURN | KeySym.KP_ENTER): self.bsp_generate() case tcod.event.KeyDown(sym=KeySym.SPACE): self.bsp_refresh() case tcod.event.KeyDown(sym=KeySym.EQUALS | KeySym.KP_PLUS): self.bsp_depth += 1 self.bsp_generate() case tcod.event.KeyDown(sym=KeySym.MINUS | KeySym.KP_MINUS): self.bsp_depth = max(1, self.bsp_depth - 1) self.bsp_generate() case tcod.event.KeyDown(sym=KeySym.N8 | KeySym.KP_MULTIPLY): self.bsp_min_room_size += 1 self.bsp_generate() case tcod.event.KeyDown(sym=KeySym.SLASH | KeySym.KP_DIVIDE): self.bsp_min_room_size = max(2, self.bsp_min_room_size - 1) self.bsp_generate() case tcod.event.KeyDown(sym=KeySym.N1 | KeySym.KP_1): self.bsp_random_room = not self.bsp_random_room if not self.bsp_random_room: self.bsp_room_walls = True self.bsp_refresh() case tcod.event.KeyDown(sym=KeySym.N2 | KeySym.KP_2): self.bsp_room_walls = not self.bsp_room_walls self.bsp_refresh() case _: super().on_event(event) class ImageSample(Sample): name = "Image toolkit" def __init__(self) -> None: self.img = tcod.image.Image.from_file(DATA_DIR / "img/skull.png") self.img.set_key_color(BLACK) self.circle = tcod.image.Image.from_file(DATA_DIR / "img/circle.png") def on_draw(self) -> None: sample_console.clear() x = sample_console.width / 2 + math.cos(time.time()) * 10.0 y = sample_console.height / 2 scalex = 0.2 + 1.8 * (1.0 + math.cos(time.time() / 2)) / 2.0 scaley = scalex angle = _get_elapsed_time() if int(time.time()) % 2: # split the color channels of circle.png # the red channel sample_console.draw_rect(0, 3, 15, 15, 0, None, (255, 0, 0)) self.circle.blit_rect(sample_console, 0, 3, -1, -1, libtcodpy.BKGND_MULTIPLY) # the green channel sample_console.draw_rect(15, 3, 15, 15, 0, None, (0, 255, 0)) self.circle.blit_rect(sample_console, 15, 3, -1, -1, libtcodpy.BKGND_MULTIPLY) # the blue channel sample_console.draw_rect(30, 3, 15, 15, 0, None, (0, 0, 255)) self.circle.blit_rect(sample_console, 30, 3, -1, -1, libtcodpy.BKGND_MULTIPLY) else: # render circle.png with normal blitting self.circle.blit_rect(sample_console, 0, 3, -1, -1, libtcodpy.BKGND_SET) self.circle.blit_rect(sample_console, 15, 3, -1, -1, libtcodpy.BKGND_SET) self.circle.blit_rect(sample_console, 30, 3, -1, -1, libtcodpy.BKGND_SET) self.img.blit(sample_console, x, y, libtcodpy.BKGND_SET, scalex, scaley, angle) class MouseSample(Sample): name = "Mouse support" def __init__(self) -> None: self.motion = tcod.event.MouseMotion() self.log: list[str] = [] def on_enter(self) -> None: sdl_window = context.sdl_window if sdl_window: tcod.sdl.mouse.warp_in_window(sdl_window, 320, 200) tcod.sdl.mouse.show(True) def on_draw(self) -> None: sample_console.clear(bg=GREY) mouse_state = tcod.event.get_mouse_state() sample_console.print( 1, 1, f"Pixel position : {mouse_state.position.x:4.0f}x{mouse_state.position.y:4.0f}\n" f"Tile position : {self.motion.tile.x:4d}x{self.motion.tile.y:4d}\n" f"Tile movement : {self.motion.tile_motion.x:4d}x{self.motion.tile_motion.y:4d}\n" f"Left button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.LEFT else 'OFF'}\n" f"Middle button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.MIDDLE else 'OFF'}\n" f"Right button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.RIGHT else 'OFF'}\n" f"X1 button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.X1 else 'OFF'}\n" f"X2 button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.X2 else 'OFF'}\n", fg=LIGHT_YELLOW, bg=None, ) sample_console.print( 1, 10, "1 : Hide cursor\n2 : Show cursor", fg=LIGHT_YELLOW, bg=None, ) def on_event(self, event: tcod.event.Event) -> None: match event: case tcod.event.MouseMotion(): self.motion = event case tcod.event.KeyDown(sym=KeySym.N1): tcod.sdl.mouse.show(visible=False) case tcod.event.KeyDown(sym=KeySym.N2): tcod.sdl.mouse.show(visible=True) case _: super().on_event(event) class NameGeneratorSample(Sample): name = "Name generator" def __init__(self) -> None: self.current_set = 0 self.delay = 0.0 self.names: list[str] = [] self.sets: list[str] = [] def on_draw(self) -> None: if not self.sets: # parse all *.cfg files in data/namegen for file in (DATA_DIR / "namegen").iterdir(): if file.suffix == ".cfg": libtcodpy.namegen_parse(file) # get the sets list self.sets = libtcodpy.namegen_get_sets() print(self.sets) while len(self.names) > 15: self.names.pop(0) sample_console.clear(bg=GREY) sample_console.print( 1, 1, f"{self.sets[self.current_set]}\n\n+ : next generator\n- : prev generator", fg=WHITE, bg=None, ) for i in range(len(self.names)): sample_console.print( SAMPLE_SCREEN_WIDTH - 1, 2 + i, self.names[i], fg=WHITE, bg=None, alignment=libtcodpy.RIGHT, ) self.delay += frame_length[-1] if self.delay > 0.5: self.delay -= 0.5 self.names.append(libtcodpy.namegen_generate(self.sets[self.current_set])) def on_event(self, event: tcod.event.Event) -> None: match event: case tcod.event.KeyDown(sym=KeySym.EQUALS): self.current_set += 1 self.names.append("======") case tcod.event.KeyDown(sym=KeySym.MINUS): self.current_set -= 1 self.names.append("======") case _: super().on_event(event) self.current_set %= len(self.sets) ############################################# # python fast render sample ############################################# SCREEN_W = SAMPLE_SCREEN_WIDTH SCREEN_H = SAMPLE_SCREEN_HEIGHT HALF_W = SCREEN_W // 2 HALF_H = SCREEN_H // 2 RES_U = 80 # texture resolution RES_V = 80 TEX_STRETCH = 5 # texture stretching with tunnel depth SPEED = 15 LIGHT_BRIGHTNESS = 3.5 # brightness multiplier for all lights (changes their radius) LIGHTS_CHANCE = 0.07 # chance of a light appearing MAX_LIGHTS = 6 MIN_LIGHT_STRENGTH = 0.2 LIGHT_UPDATE = 0.05 # how much the ambient light changes to reflect current light sources AMBIENT_LIGHT = 0.8 # brightness of tunnel texture # the coordinates of all tiles in the screen, as numpy arrays. # example: (4x3 pixels screen) # xc = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]] # noqa: ERA001 # yc = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] # noqa: ERA001 (xc, yc) = np.meshgrid(range(SCREEN_W), range(SCREEN_H)) # translate coordinates of all pixels to center xc = xc - HALF_W yc = yc - HALF_H @dataclass(frozen=False, slots=True) class Light: """Lighting effect entity.""" x: float # pos y: float z: float r: int # color g: int b: int strength: float # between 0 and 1, defines brightness class FastRenderSample(Sample): name = "Python fast render" def __init__(self) -> None: self.texture = np.zeros((RES_U, RES_V)) self.noise2d = tcod.noise.Noise(2, hurst=0.5, lacunarity=2.0) def on_enter(self) -> None: sample_console.clear() # render status message sample_console.print(1, SCREEN_H - 3, "Renderer: NumPy", fg=WHITE, bg=None) # time is represented in number of pixels of the texture, start later # in time to initialize texture self.frac_t: float = RES_V - 1 self.abs_t: float = RES_V - 1 # light and current color of the tunnel texture self.lights: list[Light] = [] self.tex_r = 0.0 self.tex_g = 0.0 self.tex_b = 0.0 def on_draw(self) -> None: time_delta = frame_length[-1] * SPEED # advance time self.frac_t += time_delta # increase fractional (always < 1.0) time self.abs_t += time_delta # increase absolute elapsed time # integer time units that passed this frame (number of texture pixels # to advance) int_t = int(self.frac_t) self.frac_t %= 1.0 # keep this < 1.0 # change texture color according to presence of lights (basically, sum # them to get ambient light and smoothly change the current color into # that) ambient_r = AMBIENT_LIGHT * sum(light.r * light.strength for light in self.lights) ambient_g = AMBIENT_LIGHT * sum(light.g * light.strength for light in self.lights) ambient_b = AMBIENT_LIGHT * sum(light.b * light.strength for light in self.lights) alpha = LIGHT_UPDATE * time_delta self.tex_r = self.tex_r * (1 - alpha) + ambient_r * alpha self.tex_g = self.tex_g * (1 - alpha) + ambient_g * alpha self.tex_b = self.tex_b * (1 - alpha) + ambient_b * alpha if int_t >= 1: # roll texture (ie, advance in tunnel) according to int_t # can't roll more than the texture's size (can happen when # time_delta is large) int_t = int_t % RES_V # new pixels are based on absolute elapsed time int_abs_t = int(self.abs_t) self.texture = np.roll(self.texture, -int_t, 1) # replace new stretch of texture with new values for v in range(RES_V - int_t, RES_V): for u in range(RES_U): tex_v = (v + int_abs_t) / float(RES_V) self.texture[u, v] = libtcodpy.noise_get_fbm( self.noise2d, [u / float(RES_U), tex_v], 32.0 ) + libtcodpy.noise_get_fbm(self.noise2d, [1 - u / float(RES_U), tex_v], 32.0) # squared distance from center, # clipped to sensible minimum and maximum values sqr_dist = xc**2 + yc**2 sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V**2) # one coordinate into the texture, represents depth in the tunnel vv = TEX_STRETCH * float(RES_V) / sqr_dist + self.frac_t vv = vv.clip(0, RES_V - 1) # another coordinate, represents rotation around the tunnel uu = np.mod(RES_U * (np.arctan2(yc, xc) / (2 * np.pi) + 0.5), RES_U) # retrieve corresponding pixels from texture brightness = self.texture[uu.astype(int), vv.astype(int)] / 4.0 + 0.5 # use the brightness map to compose the final color of the tunnel rr = brightness * self.tex_r gg = brightness * self.tex_g bb = brightness * self.tex_b # create new light source if random.random() <= time_delta * LIGHTS_CHANCE and len(self.lights) < MAX_LIGHTS: x = random.uniform(-0.5, 0.5) y = random.uniform(-0.5, 0.5) strength = random.uniform(MIN_LIGHT_STRENGTH, 1.0) color = libtcodpy.Color(0, 0, 0) # create bright colors with random hue hue = random.uniform(0, 360) libtcodpy.color_set_hsv(color, hue, 0.5, strength) self.lights.append(Light(x, y, TEX_STRETCH, color.r, color.g, color.b, strength)) # eliminate lights that are going to be out of view self.lights = [light for light in self.lights if light.z - time_delta > 1.0 / RES_V] for light in self.lights: # render lights # move light's Z coordinate with time, then project its XYZ # coordinates to screen-space light.z -= time_delta / TEX_STRETCH xl = light.x / light.z * SCREEN_H yl = light.y / light.z * SCREEN_H # calculate brightness of light according to distance from viewer # and strength, then calculate brightness of each pixel with # inverse square distance law light_brightness = LIGHT_BRIGHTNESS * light.strength * (1.0 - light.z / TEX_STRETCH) brightness = light_brightness / ((xc - xl) ** 2 + (yc - yl) ** 2) # make all pixels shine around this light rr += brightness * light.r gg += brightness * light.g bb += brightness * light.b # truncate values rr = rr.clip(0, 255) gg = gg.clip(0, 255) bb = bb.clip(0, 255) # fill the screen with these background colors sample_console.bg.transpose(2, 0, 1)[...] = (rr, gg, bb) ############################################# # main loop ############################################# RENDERER_KEYS = { tcod.event.KeySym.F1: libtcodpy.RENDERER_GLSL, tcod.event.KeySym.F2: libtcodpy.RENDERER_OPENGL, tcod.event.KeySym.F3: libtcodpy.RENDERER_SDL, tcod.event.KeySym.F4: libtcodpy.RENDERER_SDL2, tcod.event.KeySym.F5: libtcodpy.RENDERER_OPENGL2, } RENDERER_NAMES = ( "F1 GLSL ", "F2 OPENGL ", "F3 SDL ", "F4 SDL2 ", "F5 OPENGL2", ) SAMPLES = ( TrueColorSample(), OffscreenConsoleSample(), LineDrawingSample(), NoiseSample(), FOVSample(), PathfindingSample(), BSPSample(), ImageSample(), MouseSample(), NameGeneratorSample(), FastRenderSample(), ) def init_context(renderer: int) -> None: """Setup or reset a global context with common parameters set. This function exists to more easily switch between renderers. """ global context, console_render, sample_minimap if "context" in globals(): context.close() libtcod_version = ( f"{tcod.cffi.lib.TCOD_MAJOR_VERSION}.{tcod.cffi.lib.TCOD_MINOR_VERSION}.{tcod.cffi.lib.TCOD_PATCHLEVEL}" ) context = tcod.context.new( columns=root_console.width, rows=root_console.height, title=f"""python-tcod samples (python-tcod {importlib.metadata.version("tcod")}, libtcod {libtcod_version})""", vsync=False, # VSync turned off since this is for benchmarking. tileset=tileset, ) if context.sdl_renderer: # If this context supports SDL rendering. # Start by setting the logical size so that window resizing doesn't break anything. context.sdl_renderer.set_logical_presentation( resolution=(tileset.tile_width * root_console.width, tileset.tile_height * root_console.height), mode=tcod.sdl.render.LogicalPresentation.STRETCH, ) assert context.sdl_atlas # Generate the console renderer and minimap. console_render = tcod.render.SDLConsoleRender(context.sdl_atlas) sample_minimap = context.sdl_renderer.new_texture( SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT, format=tcod.cffi.lib.SDL_PIXELFORMAT_RGB24, access=tcod.sdl.render.TextureAccess.STREAMING, # Updated every frame. ) def main() -> None: global tileset tileset = tcod.tileset.load_tilesheet(FONT, 32, 8, tcod.tileset.CHARMAP_TCOD) init_context(libtcodpy.RENDERER_SDL2) try: SAMPLES[cur_sample].on_enter() while True: redraw_display() handle_time() handle_events() finally: # Normally context would be used in a with block and closed # automatically. but since this context might be switched to one with a # different renderer it is closed manually here. context.close() def redraw_display() -> None: """Full clear-draw-present of the screen.""" root_console.clear() draw_samples_menu() draw_renderer_menu() # render the sample SAMPLES[cur_sample].on_draw() sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y) draw_stats() if 0 <= mouse_tile_xy[0] < root_console.width and 0 <= mouse_tile_xy[1] < root_console.height: root_console.rgb[["fg", "bg"]].T[mouse_tile_xy] = (0, 0, 0), (255, 255, 255) # Highlight mouse tile if context.sdl_renderer: # Clear the screen to ensure no garbage data outside of the logical area is displayed context.sdl_renderer.draw_color = (0, 0, 0, 255) context.sdl_renderer.clear() # SDL renderer support, upload the sample console background to a minimap texture. sample_minimap.update(sample_console.rgb["bg"]) # Render the root_console normally, this is the drawing step of context.present without presenting. context.sdl_renderer.copy(console_render.render(root_console)) # Render the minimap to the screen. context.sdl_renderer.copy( sample_minimap, dest=( tileset.tile_width * 24, tileset.tile_height * 36, SAMPLE_SCREEN_WIDTH * 3, SAMPLE_SCREEN_HEIGHT * 3, ), ) context.sdl_renderer.present() else: # No SDL renderer, just use plain context rendering. context.present(root_console) def handle_time() -> None: if len(frame_times) > 100: frame_times.pop(0) frame_length.pop(0) frame_times.append(time.perf_counter()) frame_length.append(frame_times[-1] - frame_times[-2]) mouse_tile_xy = (-1, -1) """Last known mouse tile position.""" def handle_events() -> None: global mouse_tile_xy for event in tcod.event.get(): tile_event = tcod.event.convert_coordinates_from_window(event, context, root_console) SAMPLES[cur_sample].on_event(tile_event) match tile_event: case tcod.event.MouseMotion(integer_position=(x, y)): mouse_tile_xy = x, y case tcod.event.WindowEvent(type="WindowLeave"): mouse_tile_xy = -1, -1 def draw_samples_menu() -> None: for i, sample in enumerate(SAMPLES): if i == cur_sample: fg = WHITE bg = LIGHT_BLUE else: fg = GREY bg = BLACK root_console.print( 2, 46 - (len(SAMPLES) - i), f" {sample.name.ljust(19)}", fg, bg, alignment=libtcodpy.LEFT, ) def draw_stats() -> None: try: fps = 1 / (sum(frame_length) / len(frame_length)) except ZeroDivisionError: fps = 0 root_console.print( root_console.width, 46, f"last frame :{frame_length[-1] * 1000.0:5.1f} ms ({int(fps):4d} fps)", fg=GREY, alignment=libtcodpy.RIGHT, ) root_console.print( root_console.width, 47, f"elapsed : {int(_get_elapsed_time() * 1000):8d} ms {_get_elapsed_time():5.2f}s", fg=GREY, alignment=libtcodpy.RIGHT, ) def draw_renderer_menu() -> None: root_console.print( 42, 46 - (libtcodpy.NB_RENDERERS + 1), "Renderer :", fg=GREY, bg=BLACK, ) for i, name in enumerate(RENDERER_NAMES): if i == context.renderer_type: fg = WHITE bg = LIGHT_BLUE else: fg = GREY bg = BLACK root_console.print(42, 46 - libtcodpy.NB_RENDERERS + i, name, fg, bg) if __name__ == "__main__": if not sys.warnoptions: warnings.simplefilter("default") # Show all warnings. @tcod.event.add_watch def _handle_events(event: tcod.event.Event) -> None: """Keep window responsive during resize events.""" match event: case tcod.event.WindowEvent(type="WindowExposed"): redraw_display() handle_time() main()