diff --git a/CMakeLists.txt b/CMakeLists.txt index 24a3073e0..48a45e4b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,9 @@ message(STATUS "Building nle backend version: ${CMAKE_NLE_VERSION}") set(CMAKE_POSITION_INDEPENDENT_CODE ON) +add_compile_options(-Wno-deprecated-non-prototype) +add_compile_options(-Wno-unused-variable) + set(HACKDIR "$ENV{HOME}/nethackdir.nle" CACHE STRING "Configuration files for nethack") @@ -78,8 +81,7 @@ add_compile_definitions( HACKDIR="${HACKDIR}" DEFAULT_WINDOW_SYS="rl" DLB - NOCWD_ASSUMPTIONS - NLE_USE_TILES) + NOCWD_ASSUMPTIONS) set(NLE_SRC ${nle_SOURCE_DIR}/src) set(NLE_INC ${nle_SOURCE_DIR}/include) @@ -140,6 +142,10 @@ set_target_properties(tmt PROPERTIES C_STANDARD 11) add_library(nethack SHARED ${NETHACK_SRC}) add_dependencies(nethack util dat) set_target_properties(nethack PROPERTIES CXX_STANDARD 14 SUFFIX ".so") +target_compile_options( + nethack + PRIVATE -Wno-deprecated-non-prototype + PRIVATE -Wno-unused-variable) target_include_directories( nethack PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${NLE_INC_GEN} /usr/local/include @@ -176,8 +182,8 @@ pybind11_add_module( $) target_link_libraries(_pynethack PUBLIC nethackdl) set_target_properties(_pynethack PROPERTIES CXX_STANDARD 14) -target_include_directories(_pynethack PUBLIC ${NLE_INC_GEN}) -add_dependencies(_pynethack util) # For pm.h. +target_include_directories(_pynethack PUBLIC ${NLE_INC_GEN} ${NLE_WIN}/share) +# add_dependencies(_pynethack util tile) # For pm.h. # ttyrec converter library add_library( @@ -204,6 +210,10 @@ set_target_properties(_pyconverter PROPERTIES CXX_STANDARD 14) target_include_directories( _pyconverter PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/converter) +set(TILE_FILES "win/share/monsters.txt" "win/share/objects.txt" + "win/share/other.txt") + +install(FILES ${TILE_FILES} DESTINATION ${INSTDIR}/tiles) # Only install if we are building as part of a Python project. if(DEFINED SKBUILD_PROJECT_VERSION) install( diff --git a/dat/nle/nethack_tiles_animation.gif b/dat/nle/nethack_tiles_animation.gif new file mode 100644 index 000000000..57606117f Binary files /dev/null and b/dat/nle/nethack_tiles_animation.gif differ diff --git a/dat/nle/tileset.png b/dat/nle/tileset.png new file mode 100644 index 000000000..e46e17f25 Binary files /dev/null and b/dat/nle/tileset.png differ diff --git a/doc/nle/source/index.rst b/doc/nle/source/index.rst index 776bd8357..f58bec644 100644 --- a/doc/nle/source/index.rst +++ b/doc/nle/source/index.rst @@ -16,6 +16,7 @@ resembles the one used by people when playing the game. :caption: Getting Started getting_started + tiles .. toctree:: diff --git a/doc/nle/source/tiles.rst b/doc/nle/source/tiles.rst new file mode 100644 index 000000000..69cb3c9c6 --- /dev/null +++ b/doc/nle/source/tiles.rst @@ -0,0 +1,109 @@ +Using NetHack's tiles for image observations +============================================ + +Tiles +***** + +NetHack as we know and love is a text based game with characters representing the dungeon +levels and objects. The RL Environment represents these in an observation numpy array: + +.. code-block:: python + + obs[0]["chars"] + +Each character is also associated with a unique glyph id, which is represented in the "glyph" observation: + +.. code-block:: python + + obs[0]["glyph"] + +NetHack also contains a set of tile descriptor files which can be used to generate +the equivalent RGB values so that the game can be rendered as an image-based display. + +The source for the descriptor files are here: + +.. code-block:: console + + win/share/monsters.txt + win/share/objects.txt + win/share/other.txt + +When converted to RGB, the full set of tiles looks like this: + +.. image:: https://github.com/NetHack-LE/nle/raw/main/dat/nle/tileset.png + :alt: NetHack tileset + :align: center +\ + +Installation +************ + +The tile descriptor files are included in the distribution and are installed in the +`nethackdir/tiles` directory. + + +Initialisation +************** + +To get NLE to render the tiles as an observation set, you must set the render_mode to +"pixel" when the environment is created. For example: + +.. code-block:: python + + env = gym.make("NetHack-v0", render_mode="pixel") + +The next step is to add the Gymnasium RenderObservationWrapper to the environment. This +ensures that every time the envrionment is rendered, the observations will include the +RGB tile observations automatically. + +.. code-block:: python + + env = gym.wrappers.AddRenderObservation( + env, render_only=False, render_key="pixel", obs_key="glyphs") + +RGB observations +**************** + +The RGB tiles representing the underlying dungeon can be accessed using the "pixel" +key in the observations dictionary. + +.. code-block:: python + + rgb_frame = obs[0]["pixel"] + +This frame is a 3D numpy array, each 2D slice represents all the pixels in the +rendered game screen, and the 3rd dimension represents the RGB values for each pixel. + +Example +******* + +Here's a short example of how to set up the environment to use the tile-based RGB observations: + +(Note that you need to install the Pillow library to run this example, which can be done with `pip install Pillow`) + +.. code-block:: python + + import gymnasium as gym + from PIL import Image + import nle + + env = gym.make("NetHack-v0", render_mode="pixel") + env = gym.wrappers.AddRenderObservation( + env, render_only=False, render_key="pixel", obs_key="glyphs") + env.unwrapped.seed(1234, 5678, False, 1) + + obs, _ = env.reset() # each reset generates a new dungeon + rgb_frame = obs["pixel"] + + # Convert the RGB frame to an image and display it + img = Image.fromarray(rgb_frame) + img.show() + + +Here's an animated example of what the tile-based rendering looks like +after a few steps in the environment: + +.. image:: https://github.com/NetHack-LE/nle/raw/main/dat/nle/nethack_tiles_animation.gif + :alt: NetHack tileset + :align: center +\ diff --git a/include/nletypes.h b/include/nletypes.h index e1edefdfc..824d41df1 100644 --- a/include/nletypes.h +++ b/include/nletypes.h @@ -46,8 +46,6 @@ #define NLE_BL_CONDITION 25 /* condition bit mask */ #define NLE_BL_ALIGN 26 -/* #define NLE_USE_TILES 1 */ /* Set in CMakeLists.txt. */ - /* NetHack defines boolean as follows: typedef xchar boolean; (global.h:80) typedef schar xchar; (global.h:73) diff --git a/nle/env/base.py b/nle/env/base.py index 3cbbbb0f1..8c7a77f34 100644 --- a/nle/env/base.py +++ b/nle/env/base.py @@ -151,7 +151,7 @@ class NLE(gym.Env): # but NetHack doesn't have any. Set it to 42, because # that is always the answer to life, the universe and # everything. - metadata = {"render_modes": ["human", "ansi", "full"], "render_fps": 42} + metadata = {"render_modes": ["human", "ansi", "full", "pixel"], "render_fps": 42} class StepStatus(enum.IntEnum): """Specifies the status of the terminal state. @@ -332,6 +332,12 @@ def __init__( self.action_space = gym.spaces.Discrete(len(self.actions)) + if render_mode == "pixel": + # Pre-load reference tilemap for pixel rendering + if not self.nethack.setup_tiles(): + raise RuntimeError("Failed to setup tilemap for pixel rendering.") + self.rendered_frame = np.zeros(nethack.TILE_RENDER_SHAPE, dtype=np.uint8) + def _get_observation(self, observation): return { key: observation[i] @@ -549,6 +555,10 @@ def render(self): # TODO: Why return a string here but print in the other branches? return "\n".join([line.tobytes().decode("utf-8") for line in chars]) + if mode == "pixel": + self.nethack.draw_frame(buffer=self.rendered_frame) + return self.rendered_frame + return "\nInvalid render mode: " + mode def __repr__(self): diff --git a/nle/nethack/__init__.py b/nle/nethack/__init__.py index d456c7fa4..a80008b04 100644 --- a/nle/nethack/__init__.py +++ b/nle/nethack/__init__.py @@ -9,6 +9,8 @@ from nle.nethack.nethack import NETHACKOPTIONS from nle.nethack.nethack import OBSERVATION_DESC from nle.nethack.nethack import PROGRAM_STATE_SHAPE +from nle.nethack.nethack import TILE_RENDER_SHAPE +from nle.nethack.nethack import TILE_SHAPE from nle.nethack.nethack import TTYREC_VERSION from nle.nethack.nethack import Nethack from nle.nethack.nethack import tty_render diff --git a/nle/nethack/nethack.py b/nle/nethack/nethack.py index c5c368ec7..8bc0a808c 100644 --- a/nle/nethack/nethack.py +++ b/nle/nethack/nethack.py @@ -14,6 +14,16 @@ HACKDIR = os.path.join(os.path.dirname(_pynethack.__file__), "nethackdir") DUNGEON_SHAPE = (_pynethack.nethack.ROWNO, _pynethack.nethack.COLNO - 1) +TILE_SHAPE = ( + _pynethack.nethack.TILE_Y, + _pynethack.nethack.TILE_X, + _pynethack.nethack.TILE_Z, +) +TILE_RENDER_SHAPE = ( + DUNGEON_SHAPE[0] * TILE_SHAPE[0], + DUNGEON_SHAPE[1] * TILE_SHAPE[1], + TILE_SHAPE[2], +) BLSTATS_SHAPE = (_pynethack.nethack.NLE_BLSTATS_SIZE,) MESSAGE_SHAPE = (_pynethack.nethack.NLE_MESSAGE_SIZE,) PROGRAM_STATE_SHAPE = (_pynethack.nethack.NLE_PROGRAM_STATE_SIZE,) @@ -318,3 +328,18 @@ def in_normal_game(self): def how_done(self): return self._pynethack.how_done() + + def setup_tiles(self, tile_paths=None): + if tile_paths is None: + tile_paths = [ + os.path.join(HACKDIR, "tiles", "monsters.txt"), + os.path.join(HACKDIR, "tiles", "objects.txt"), + os.path.join(HACKDIR, "tiles", "other.txt"), + ] + return self._pynethack.setup_tiles(tile_paths) + + def get_tileset(self, buffer): + return self._pynethack.get_tileset(buffer) + + def draw_frame(self, buffer): + return self._pynethack.draw_frame(buffer) diff --git a/nle/scripts/pixels.py b/nle/scripts/pixels.py new file mode 100644 index 000000000..33b4fef22 --- /dev/null +++ b/nle/scripts/pixels.py @@ -0,0 +1,113 @@ +import gymnasium as gym +from PIL import Image + +import nle # noqa: F401 + +env = gym.make("NetHack-v0", render_mode="pixel") +env = gym.wrappers.AddRenderObservation( + env, render_only=False, render_key="pixel", obs_key="glyphs" +) +env.unwrapped.seed(1234, 5678, False, 1) + + +frames = [] + +obs = env.reset() +frame = obs[0]["pixel"] +img = Image.fromarray(frame, "RGB") +frames.append(img) + +NORTH = 0 +WEST = 3 +SOUTH = 2 +EAST = 1 + +# Get out of starting room +steps = [EAST, EAST, NORTH, NORTH, WEST, WEST, WEST, WEST, WEST] +# Go to room two +steps += [ + SOUTH, + SOUTH, + SOUTH, + SOUTH, + SOUTH, + WEST, + SOUTH, + WEST, + SOUTH, + SOUTH, + SOUTH, + SOUTH, + SOUTH, + WEST, + SOUTH, + WEST, + WEST, +] +# Traverse room two +steps += [WEST, WEST, WEST, WEST, NORTH, NORTH, NORTH, NORTH] +# Go to room three +steps += [ + WEST, + NORTH, + NORTH, + WEST, + WEST, + NORTH, + NORTH, + WEST, + WEST, + NORTH, + NORTH, + WEST, + WEST, + WEST, + WEST, + WEST, + WEST, + WEST, + WEST, + NORTH, +] +# Traverse room three +steps += [NORTH, NORTH] +# Go back towards room two and kill the monster +steps += [SOUTH, SOUTH, SOUTH, EAST, EAST, EAST, EAST, EAST] +# Continue to room two +steps += [ + EAST, + EAST, + EAST, + SOUTH, + SOUTH, + EAST, + EAST, + SOUTH, + SOUTH, + EAST, + EAST, + SOUTH, + SOUTH, + EAST, + SOUTH, + SOUTH, +] +# Traverse room two to other exit +steps += [WEST, WEST, WEST, WEST, WEST, WEST, WEST, WEST, WEST, NORTH] +# Go to room four +steps += [NORTH, NORTH, NORTH, NORTH] + +for action in range(len(steps)): + obs = env.step(steps[action]) + env.unwrapped.nethack.draw_frame(frame) + img = Image.fromarray(frame, "RGB") + frames.append(img) + +print(f"Saving animation with {len(frames)} frames...") +frames[0].save( + "nethack_tiles_animation.gif", + save_all=True, + append_images=frames[1:], + duration=400, + loop=0, +) diff --git a/nle/tests/test_tiles.py b/nle/tests/test_tiles.py new file mode 100644 index 000000000..42d866c8a --- /dev/null +++ b/nle/tests/test_tiles.py @@ -0,0 +1,96 @@ +import gymnasium as gym +import numpy as np +import pytest + +import nle + + +class TestTileset: + # Test that the tile files can be read successfully + # with the default paths & that the tileset is correctly + # generated. + def test_tile_setup_repo(self): + nh = nle.nethack.Nethack() + nh.setup_tiles() + tileset = np.zeros((432, 640, 3), dtype=np.uint8) + nh.get_tileset(tileset) + + assert tileset[0][0][0] == 71 + assert tileset[431][638][2] == 108 + + # Test that invalid tile paths raise an error. + def test_tile_setup_invalid_path(self): + nh = nle.nethack.Nethack() + with pytest.raises(RuntimeError): + nh.setup_tiles( + [ + "invalid/path/monsters.txt", + "invalid/path/objects.txt", + "invalid/path/other.txt", + ] + ) + + # Stupid test but am doing it anyway :-) + # Test that the tileset cannot be retrieved if the frame is too big + def test_tileset_too_large(self): + nh = nle.nethack.Nethack() + nh.setup_tiles() + tileset = np.zeros((1000, 1000, 3), dtype=np.uint8) + with pytest.raises(RuntimeError): + nh.get_tileset(tileset) + + # Alternatively, test that the tileset can be retrieved if the frame is too small. + def test_tileset_too_small(self): + nh = nle.nethack.Nethack() + nh.setup_tiles() + tileset = np.zeros((100, 100, 3), dtype=np.uint8) + nh.get_tileset(tileset) + + assert tileset[0][0][0] == 71 + assert tileset[99][99][2] == 0 + + +class TestDrawingFrame: + def test_drawing_frame_before_tileset_setup(self): + nh = nle.nethack.Nethack() + frame = np.zeros(nle.nethack.TILE_RENDER_SHAPE, dtype=np.uint8) + with pytest.raises(RuntimeError): + nh.draw_frame(frame) + + def test_drawing_frame_with_invalid_frame_size(self): + nh = nle.nethack.Nethack() + nh.setup_tiles() + frame = np.zeros((100, 100, 3), dtype=np.uint8) + with pytest.raises(ValueError): + nh.draw_frame(frame) + + +class TestTileObservations: + def test_observation_contains_pixels(self): + env = gym.make("NetHack-v0", render_mode="pixel") + env = gym.wrappers.AddRenderObservation( + env, render_only=False, render_key="pixel", obs_key="glyphs" + ) + obs = env.reset() + + assert "pixel" in obs[0] + + # Test that the observation contains the correct pixel data for the starting location of the hero. + def test_hero_pixel_values(self): + env = gym.make("NetHack-v0", render_mode="pixel") + env = gym.wrappers.AddRenderObservation( + env, render_only=False, render_key="pixel", obs_key="glyphs" + ) + env.unwrapped.seed(1234, 5678, False, 1) + + obs = env.reset() + + # The monk hero should be at location (7,51) with this seed. + assert obs[0]["chars"][7][51] == ord("@") + assert obs[0]["glyphs"][7][51] == 333 + + # The pixel location corresponding to (7,51) is (7*16, 51*16) = (112, 816). + # Pick out the R values of the piles a few rows down to check that the correct tiles are being rendered. + assert obs[0]["pixel"][115][822][0] == 145 + assert obs[0]["pixel"][115][823][0] == 255 + assert obs[0]["pixel"][115][824][0] == 145 diff --git a/util/CMakeLists.txt b/util/CMakeLists.txt index 8f0aa828e..4f0194b67 100644 --- a/util/CMakeLists.txt +++ b/util/CMakeLists.txt @@ -54,11 +54,13 @@ add_custom_command( DEPENDS makedefs OUTPUT ${NLE_INC_GEN}/pm.h COMMAND $ ARGS -p) +add_custom_target(generate_pm_h DEPENDS ${NLE_INC_GEN}/pm.h) add_custom_command( DEPENDS tilemap OUTPUT ${NLE_SRC_GEN}/tile.c COMMAND $) +add_custom_target(generate_tile_c DEPENDS ${NLE_SRC_GEN}/tile.c) add_custom_command( OUTPUT ${NLE_UTIL_GEN}/dgn_parser.c ${NLE_UTIL_GEN}/dgn_comp.h @@ -90,8 +92,12 @@ add_executable(tilemap ${NLE_WIN}/share/tilemap.c) target_include_directories(tilemap PUBLIC ${NLE_INC} ${NLE_INC_GEN}) add_dependencies(tilemap util) -add_library(tile OBJECT ${NLE_SRC_GEN}/tile.c) -target_include_directories(tile PUBLIC ${NLE_INC} ${NLE_INC_GEN}) +file(GLOB NETHACK_TILE_SRC ${NLE_WIN}/rl/tile2rgb.c ${NLE_WIN}/rl/nletiletxt.c + ${NLE_WIN}/share/tiletext.c) +add_library(tile OBJECT ${NETHACK_TILE_SRC} ${NLE_SRC_GEN}/tile.c) +target_include_directories(tile PUBLIC ${NLE_INC} ${NLE_INC_GEN} + ${NLE_WIN}/share) +add_dependencies(tile generate_pm_h generate_tile_c) # NOTE: util is dependent on these two add_dependencies(lev_comp util) diff --git a/win/rl/nletiletxt.c b/win/rl/nletiletxt.c new file mode 100644 index 000000000..87c4865d1 --- /dev/null +++ b/win/rl/nletiletxt.c @@ -0,0 +1,11 @@ +/* Copied from NetHack's /util/Makefile.utl + +This is the secondary compilation of tilemap.c with the TILETEXT +directive set. + +*/ + +#define TILETEXT +#include "../share/tilemap.c" + +/*nletiletxt.c*/ \ No newline at end of file diff --git a/win/rl/pynethack.cc b/win/rl/pynethack.cc index 13894a60c..e4d1bbcf6 100644 --- a/win/rl/pynethack.cc +++ b/win/rl/pynethack.cc @@ -5,6 +5,7 @@ #include #include +#include // "digit" is declared in both Python's longintrepr.h and NetHack's extern.h. #define digit nethack_digit @@ -18,6 +19,7 @@ extern "C" { extern "C" { #include "nledl.h" +#include "tile2rgb.h" } // Undef name clashes between NetHack and Python. @@ -25,8 +27,8 @@ extern "C" { #undef min #undef max -#ifdef NLE_USE_TILES -extern short glyph2tile[]; /* in tile.c (made from tilemap.c) */ +extern short glyph2tile[]; /* in tile.c (made from tilemap.c) */ +extern int total_tiles_used; /* also in tile.c */ /* Copy from dungeon.c. Necessary to add tile.c. Can't add dungeon.c itself as it pulls in too much. */ @@ -53,7 +55,6 @@ on_level(d_level *lev1, d_level *lev2) && lev1->dlevel == lev2->dlevel); } /* End of copy from dungeon.c */ -#endif namespace py = pybind11; using namespace py::literals; @@ -155,6 +156,9 @@ class Nethack if (ttyrec_) { fclose(ttyrec_); } + if (tileset) { + free(tileset); + } } void @@ -363,6 +367,126 @@ class Nethack strncpy(settings_.wizkit, wizkit.c_str(), sizeof(settings_.wizkit)); } + boolean + setup_tileset(std::array tilefiles) + { + tileset = + (tile_t *) calloc(sizeof(tile_t), (size_t) total_tiles_used); + if (!tileset) { + throw std::runtime_error( + "Unable to allocate memory for tileset."); + } + + const char *tilefile_ptrs[3] = { tilefiles[0].c_str(), + tilefiles[1].c_str(), + tilefiles[2].c_str() }; + int tiles_read = init_rgb_tileset(tilefile_ptrs, 3, tileset); + + if (tiles_read != 3) { + throw std::runtime_error("Unable to open tile file " + + tilefiles[tiles_read] + + " for reading. Check that the file " + "exists and is readable."); + } + + return true; + } + + // Get the tileset as a numpy array of shape passed in as 'frame'. + // This method is for testing the initialization of the tileset only. + void + get_tileset(py::array_t frame) + { + if (!tileset) { + throw std::runtime_error("get_tileset() called but the tileset " + "has not been initialized."); + } + + auto buffer = frame.mutable_unchecked<3>(); + + pybind11::size_t tile_rows = buffer.shape(0) / TILE_Y; + pybind11::size_t tile_cols = buffer.shape(1) / TILE_X; + + if (tile_rows * tile_cols > (pybind11::size_t) total_tiles_used) { + throw std::runtime_error( + "Requested more tiles than available in tileset (available: " + + std::to_string(total_tiles_used) + ", requested: " + + std::to_string(tile_rows * tile_cols) + ")."); + return; + } + + uint8_t *pixel_rgb = (uint8_t *) tileset; + + for (pybind11::ssize_t tile_row = 0; tile_row < tile_rows; + tile_row++) { + for (pybind11::ssize_t tile_col = 0; tile_col < tile_cols; + tile_col++) { + for (pybind11::ssize_t y = 0; y < TILE_Y; y++) { + memcpy(&buffer((tile_row * TILE_Y) + y, + (tile_col * TILE_X), 0), + pixel_rgb, TILE_X * TILE_Z * sizeof(uint8_t)); + pixel_rgb += TILE_X * TILE_Z; + } + } + } + } + + void + draw_frame(py::array_t frame) + { + if (!tileset) { + throw std::runtime_error("draw_frame() called but the tileset " + "has not been initialized."); + } + + auto buffer = checked_conversion( + frame, { ROWNO * TILE_Y, (COLNO - 1) * TILE_X, TILE_Z }); + + int frame_width = (COLNO - 1) * TILE_X * TILE_Z; + + for (int tile_row = 0; tile_row < ROWNO; tile_row++) { + for (int tile_col = 0; tile_col < (COLNO - 1); tile_col++) { + // figure out which tile to copy from the glyph at this + // position + short int glyph = + obs_.glyphs[(tile_row * (COLNO - 1)) + tile_col]; + + // only update the tile if the glyph has changed since last + // time + if (glyph + == prev_glyphs[(tile_row * (COLNO - 1)) + tile_col]) { + continue; + } + int tile_index = glyph2tile[glyph]; + + // Check tile_index is within bounds of the tileset. If not, + // log and skip this tile. + if (tile_index < 0 || tile_index >= total_tiles_used) { + fprintf(stderr, + "Invalid tile index %d for glyph %d at position " + "(%ld,%ld)\n", + tile_index, glyph, tile_row, tile_col); + continue; + } + + tile_t *tile_data = &(tileset[tile_index]); + uint8_t *frame_tile = buffer + + (tile_row * frame_width * TILE_Y) + + (tile_col * TILE_X * TILE_Z); + + for (int pixel_row = 0; pixel_row < TILE_Y; pixel_row++) { + memcpy(frame_tile + (pixel_row * frame_width), + &(tile_data->tile[pixel_row][0]), + TILE_X * TILE_Z * sizeof(uint8_t)); + } + } + } + + // store glyphs for faster tile rendering next time this is called + std::copy(obs_.glyphs, obs_.glyphs + ROWNO * (COLNO - 1), + prev_glyphs); + } + private: void reset(FILE *ttyrec) @@ -382,6 +506,12 @@ class Nethack settings_.initial_seeds.use_init_seeds = false; settings_.initial_seeds.use_lgen_seed = false; + if (tileset) { + // reset previous glyphs to force full redraw on first draw_frame + // call + std::fill(std::begin(prev_glyphs), std::end(prev_glyphs), 0); + } + if (obs_.done) throw std::runtime_error("NetHack done right after reset"); } @@ -392,6 +522,8 @@ class Nethack nledl_ctx *nle_ = nullptr; std::FILE *ttyrec_ = nullptr; nle_settings settings_; + tile_t *tileset = nullptr; + short prev_glyphs[ROWNO * (COLNO - 1)] = { 0 }; }; PYBIND11_MODULE(_pynethack, m) @@ -431,7 +563,10 @@ PYBIND11_MODULE(_pynethack, m) .def("get_seeds", &Nethack::get_seeds) .def("in_normal_game", &Nethack::in_normal_game) .def("how_done", &Nethack::how_done) - .def("set_wizkit", &Nethack::set_wizkit); + .def("set_wizkit", &Nethack::set_wizkit) + .def("setup_tiles", &Nethack::setup_tileset) + .def("get_tileset", &Nethack::get_tileset) + .def("draw_frame", &Nethack::draw_frame); py::module mn = m.def_submodule( "nethack", "Collection of NetHack constants and functions"); @@ -487,6 +622,10 @@ PYBIND11_MODULE(_pynethack, m) mn.attr("NHW_MENU") = py::int_(NHW_MENU); mn.attr("NHW_TEXT") = py::int_(NHW_TEXT); + mn.attr("TILE_X") = py::int_(TILE_X); + mn.attr("TILE_Y") = py::int_(TILE_Y); + mn.attr("TILE_Z") = py::int_(TILE_Z); + // Cannot include wintty.h as it redefines putc etc. // MAXWIN is #defined as 20 there. mn.attr("MAXWIN") = py::int_(20); @@ -629,13 +768,10 @@ PYBIND11_MODULE(_pynethack, m) py::vectorize([](int glyph) { return glyph_is_swallow(glyph); })); mn.def("glyph_is_warning", py::vectorize([](int glyph) { return glyph_is_warning(glyph); })); - -#ifdef NLE_USE_TILES mn.attr("glyph2tile") = py::memoryview::from_buffer(glyph2tile, /*shape=*/{ MAX_GLYPH }, /*strides=*/{ sizeof(glyph2tile[0]) }, /*readonly=*/true); -#endif py::class_(mn, "permonst", "The permonst struct.") .def( @@ -694,7 +830,7 @@ PYBIND11_MODULE(_pynethack, m) "Argument should be between 0 and MAXMCLASSES (" + std::to_string(MAXMCLASSES) + ") but got " + std::to_string(let)); - return &def_monsyms[let]; + return &def_monsyms[(int) let]; }, py::return_value_policy::reference) .def_static( @@ -705,7 +841,7 @@ PYBIND11_MODULE(_pynethack, m) "Argument should be between 0 and MAXOCLASSES (" + std::to_string(MAXOCLASSES) + ") but got " + std::to_string(olet)); - return &def_oc_syms[olet]; + return &def_oc_syms[(int) olet]; }, py::return_value_policy::reference) .def_readonly("sym", &class_sym::sym) diff --git a/win/rl/tile2rgb.c b/win/rl/tile2rgb.c new file mode 100644 index 000000000..1d354cc5b --- /dev/null +++ b/win/rl/tile2rgb.c @@ -0,0 +1,53 @@ +/* Converts the tile text descriptions in monsters.txt, objects.txt, and + other.txt into RGB pixels */ + +#include "hack.h" +#include + +#include "tile2rgb.h" + +/* defined in tile.c, a generated file */ +extern short glyph2tile[]; +extern int total_tiles_used; + +/* +Basically want to open the files, read the pixels and be done with it. +Returns the number of files read sucessfully, so 0 == failure. +*/ +int +init_rgb_tileset(const char *filenames[], int filecount, tile_t *tileset) +{ + if (!filenames || filecount <= 0) { + // no files to read, return 0 + return 0; + } + + if (!tileset) { + // function was called without memory being allocated + return 0; + } + + pixel tile[TILE_Y][TILE_X]; + tile_t *tile_ptr = tileset; + + for (int f = 0; f < filecount; f++) { + /* file handles are static variables in tiletext.c so + we don't have to manage them - except that we call + the open and close management functions. + */ + if (!fopen_text_file(filenames[f], "r")) { + /* can't read the tiles, throw the problem back */ + fprintf(stderr, "init_tiles: unable to open %s\n", filenames[f]); + return f; + } + + while (read_text_tile(tile)) { + memcpy(tile_ptr, &(tile), TILE_Y * TILE_X * sizeof(pixel)); + tile_ptr++; + } + + fclose_text_file(); + } + + return filecount; +} \ No newline at end of file diff --git a/win/rl/tile2rgb.h b/win/rl/tile2rgb.h new file mode 100644 index 000000000..5bdf1d1ed --- /dev/null +++ b/win/rl/tile2rgb.h @@ -0,0 +1,16 @@ +/* Interface for converting the text-defined tiles to rgb array */ + +#ifndef TILE2RGB_H +#define TILE2RGB_H + +#include "tile.h" + +#define TILE_Z 3 /* RGB */ + +typedef struct tile_s { + pixel tile[TILE_Y][TILE_X]; +} tile_t; + +int init_rgb_tileset(const char *[], int, tile_t *); + +#endif /* TILE2RGB */ \ No newline at end of file