Skip to content
Open
28 changes: 25 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,23 @@ set(NTDLL ntdll.lib)
endif()

set(Boost_USE_STATIC_LIBS ON)
find_package(Boost 1.70.0 REQUIRED COMPONENTS filesystem serialization system process)
find_package(Boost 1.70.0 REQUIRED COMPONENTS filesystem serialization system)
if(LINUX)
find_package(Threads REQUIRED)
endif()

# Optional raylib support for GUI image display
option(USE_RAYLIB "Enable raylib for GUI image display" OFF)
if(USE_RAYLIB)
find_package(raylib QUIET)
if(raylib_FOUND)
message(STATUS "raylib found - GUI image display enabled")
add_compile_definitions(RAYLIB_SUPPORT)
else()
message(STATUS "raylib not found - building without GUI support")
endif()
endif()

# Make sure we have at least gcc-11.
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "11")
Expand All @@ -48,14 +60,19 @@ add_executable(zork
act3.h act3.cpp
act4.h act4.cpp
adv.h adv.cpp
ascii_art.h ascii_art.cpp
chafa_wrapper.h chafa_wrapper.cpp
raylib_wrapper.h raylib_wrapper.cpp
raylib_console.h raylib_console.cpp
cevent.h cevent.cpp

stb_image.h

defs.h defs.cpp
dung.h dung.cpp
FlagSupport.h
funcs.h funcs.cpp
globals.h globals.cpp
gobject.h
gobject.h
info.h info.cpp
makstr.h makstr.cpp
mdlfun.cpp
Expand All @@ -82,6 +99,11 @@ target_link_libraries(zork ${Boost_LIBRARIES} ${NTDLL} ${CMAKE_DL_LIBS})
if (LINUX)
target_link_libraries(zork Threads::Threads)
endif()

# Link raylib if found
if(USE_RAYLIB AND raylib_FOUND)
target_link_libraries(zork raylib)
endif()
target_precompile_headers(zork PRIVATE precomp.h)

if (${CMAKE_BUILD_TYPE} STREQUAL "Release")
Expand Down
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# README

***
This Fork update/changes (org README below)
Support for showing the AI art mentioned below in Ascii art using buildin code or improved gfx with chafa (if installed) was added together with an inital GUI app to show the images properly. Start zork wiht --gui or --ascii-art to use this modes.
A script to generate AI images that also support replacing images and edit the room description if needed to generate better images was added. It needs a huggingface token and you get some images for free, if you need/want to add money you can get a way with a very low amount, I used less then $0.20 (Jan 2026) to generate all images during development.

This might get merged back to the main project and if so this part should be folded into the text below.
***

Main project README:

***
UPDATE: Because I either have too much time on my hands, or an odd (borderline unhealthy) obsession with all of this,
I've been using AI image generators to create pictures of various locations in Zork. In a separate project
Expand Down Expand Up @@ -81,4 +91,66 @@ Boost libraries are statically-linked so there are no other dependencies.
cmake --preset linux-debug
cmake --build out/build/linux-debug
out/build/linux-debug/zork
```

## Building with GUI Support

The GUI mode uses raylib to display room images in a graphical window. To enable it:

### 1. Install raylib

**On Ubuntu/Debian:**
```bash
sudo apt install libraylib-dev
```

**On Fedora:**
```bash
sudo dnf install raylib-devel
```

**On macOS (using Homebrew):**
```bash
brew install raylib
```

**Building from source (all platforms):**
```bash
git clone https://github.com/raysan5/raylib.git
cd raylib
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
make -j4
sudo make install
```

### 2. Build with raylib enabled

```bash
# Using presets
cmake --preset linux-debug -DUSE_RAYLIB=ON
cmake --build out/build/linux-debug

# Or using direct cmake
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_RAYLIB=ON ..
make -j4
```

### 3. Run with GUI

```bash
./zork --gui
```

### Troubleshooting

If CMake reports "raylib not found", ensure raylib is installed and CMake can find it. You may need to set:
```bash
export CMAKE_PREFIX_PATH=/usr/local
```

Or specify the path directly:
```bash
cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_RAYLIB=ON -Draylib_ROOT=/path/to/raylib ..
```
198 changes: 198 additions & 0 deletions ascii_art.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#include "precomp.h"
#include "ascii_art.h"
#include "chafa_wrapper.h"
#include "raylib_wrapper.h"
#include "raylib_console.h"
#include "globals.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sys/ioctl.h>
#include <unistd.h>

// STB image library
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

namespace fs = std::filesystem;

// ASCII characters from dark to light (more detailed set)
const char* ASCII_CHARS = " .'`^\",:;Il!i~<>+_-?][}{1)(|\\//tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$";
const int NUM_ASCII_CHARS = 70;

// Get terminal size
std::pair<int, int> get_terminal_size()
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0)
{
return { ws.ws_col, ws.ws_row };
}
// Fallback to environment variables
const char* cols = getenv("COLUMNS");
const char* rows = getenv("LINES");
int width = cols ? atoi(cols) : 80;
int height = rows ? atoi(rows) : 24;
return { width, height };
}

// Try to find an image file for the room ID
std::optional<fs::path> find_image_file(const std::string& room_id)
{
const char* extensions[] = { ".png", ".jpg", ".jpeg", ".jfif", ".gif", ".bmp" };

// Check in zork_pics directory
fs::path pics_dir = "zork_pics";

// Try lowercase version of room ID
std::string lower_id = room_id;
std::transform(lower_id.begin(), lower_id.end(), lower_id.begin(), ::tolower);

for (const auto& ext : extensions)
{
fs::path img_path = pics_dir / (lower_id + ext);
if (fs::exists(img_path))
{
return img_path;
}
}

return std::nullopt;
}

// ANSI color escape code helper
std::string rgb_color(int r, int g, int b, bool foreground = true)
{
int code = foreground ? 38 : 48; // 38 = foreground, 48 = background
return "\033[" + std::to_string(code) + ";2;" + std::to_string(r) + ";" + std::to_string(g) + ";" + std::to_string(b) + "m";
}

const std::string RESET_COLOR = "\033[0m";

// Convert image to ASCII art and display it with color
bool display_room_image(const std::string& room_id)
{
auto img_path_opt = find_image_file(room_id);
if (!img_path_opt)
{
std::cerr << "No image found for room: " << room_id << std::endl;
return false;
}

fs::path img_path = *img_path_opt;
std::cerr << "Found image for room " << room_id << ": " << img_path << std::endl;

// Try GUI console first if enabled
if (flags[FlagId::gui_mode] && raylib_console_available())
{
std::cerr << "Setting image in GUI console" << std::endl;
raylib_console_set_image(img_path.string());
return true;
}

// Try GUI window if enabled (fallback for when console isn't available)
if (flags[FlagId::gui_mode] && raylib_available())
{
char result = raylib_display_image_interactive(img_path.string());
return true;
}

// Try Chafa for terminal graphics
if (chafa_available())
{
auto [term_width, term_height] = get_terminal_size();
const int margin = 4;
int max_width = term_width - margin;
int max_height = term_height - 12;

if (display_image_chafa(img_path.string(), max_width, max_height))
{
return true;
}
// Fall through to ASCII art if Chafa fails
}

// Load image using stb_image (request 3 channels for RGB)
int width, height, channels;
unsigned char* data = stbi_load(img_path.string().c_str(), &width, &height, &channels, 3);

if (!data)
{
return false;
}

// Get terminal size
auto [term_width, term_height] = get_terminal_size();

// Leave some margin for borders and text
const int margin = 4;
const int max_width = term_width - margin;
// Use most of the terminal height, but leave room for description text
const int max_height = term_height - 12;

// Calculate scaling to fill terminal while maintaining aspect ratio
// Characters are roughly 2x as tall as they are wide, so we adjust for that
double scale_x = static_cast<double>(width) / max_width;
double scale_y = static_cast<double>(height) / (max_height * 2); // Multiply by 2 for char aspect ratio
double scale = std::max(scale_x, scale_y);

if (scale < 1.0) scale = 1.0;

int new_width = static_cast<int>(width / scale);
int new_height = static_cast<int>(height / scale / 2); // Divide by 2 for character aspect ratio

if (new_width < 1) new_width = 1;
if (new_height < 1) new_height = 1;

// Center the image
int padding = (term_width - new_width - 2) / 2;
if (padding < 0) padding = 0;
std::string pad_str(padding, ' ');

// Print top border
std::cout << pad_str << "+";
for (int x = 0; x < new_width; ++x)
std::cout << "-";
std::cout << "+\n";

// Convert and print ASCII art with color
for (int y = 0; y < new_height; ++y)
{
std::cout << pad_str << "|";
for (int x = 0; x < new_width; ++x)
{
// Map from output coordinates to source image coordinates
int src_x = static_cast<int>(x * scale);
int src_y = static_cast<int>(y * 2 * scale);

if (src_x >= width) src_x = width - 1;
if (src_y >= height) src_y = height - 1;

// Get pixel RGB values
int pixel_idx = (src_y * width + src_x) * 3;
unsigned char r = data[pixel_idx];
unsigned char g = data[pixel_idx + 1];
unsigned char b = data[pixel_idx + 2];

// Calculate brightness for character selection
int brightness = static_cast<int>(0.299 * r + 0.587 * g + 0.114 * b);

// Map to ASCII character with more detail
int char_index = (brightness * (NUM_ASCII_CHARS - 1)) / 255;
char ascii_char = ASCII_CHARS[char_index];

// Output colored character
std::cout << rgb_color(r, g, b) << ascii_char << RESET_COLOR;
}
std::cout << "|\n";
}

// Print bottom border
std::cout << pad_str << "+";
for (int x = 0; x < new_width; ++x)
std::cout << "-";
std::cout << "+\n";

stbi_image_free(data);
return true;
}
7 changes: 7 additions & 0 deletions ascii_art.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#pragma once

#include <string>

// Display ASCII art for a room based on its ID
// Returns true if an image was found and displayed
bool display_room_image(const std::string& room_id);
Loading