Skip to content

Commit

Permalink
Latest
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonbrianhall committed Jan 2, 2025
1 parent 941d967 commit 174c85e
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 38 deletions.
194 changes: 194 additions & 0 deletions hd/hd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
from PIL import Image
import numpy as np
import os
import json

def extract_characters(chr_file, output_dir):
"""
Extract character bitmaps from willy.chr into individual 128x128 PNG files with transparency
"""
# Character mapping from the original code
namedpart = {
"0": "WILLY_RIGHT",
"1": "WILLY_LEFT",
"2": "PRESENT",
"3": "LADDER",
"4": "TACK",
"5": "UPSPRING",
"6": "SIDESPRING",
"7": "BALL",
"8": "BELL"
}
# Add pipe parts 51-90
for i in range(51, 91):
namedpart[str(i)] = f"PIPE{i-50}"
namedpart["126"] = "BALLPIT"
namedpart["127"] = "EMPTY"

# Create output directory if it doesn't exist
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# Read the character data file
with open(chr_file, 'rb') as f:
data = bytearray(f.read())

# Process each character from original 8x8 format
for i in range(len(data) // 8):
# Convert 8 bytes into 8x8 bitmap with alpha
bitmap = np.zeros((8, 8, 4), dtype=np.uint8)
for y in range(8):
byte = data[i * 8 + y]
for x in range(8):
pixel = (byte >> (7 - x)) & 1
if pixel:
# White pixel with full opacity
bitmap[y, x] = [255, 255, 255, 255]
else:
# Black pixel with full transparency
bitmap[y, x] = [0, 0, 0, 0]

# Create PIL image with alpha channel
img = Image.fromarray(bitmap, 'RGBA')

# Scale up to 128x128 using nearest neighbor to preserve sharp edges
img = img.resize((128, 128), Image.NEAREST)

# Save the image if it has a name
char_name = namedpart.get(str(i))
if char_name:
img.save(os.path.join(output_dir, f"{char_name}.png"), 'PNG')

def create_chr_file(input_dir, output_file):
"""
Create a new willy.chr file from 128x128 RGBA PNG images.
New format: JSON header with character mapping + concatenated raw RGBA data
"""
# Character mapping (reversed)
namedpart_reverse = {
"WILLY_RIGHT": 0,
"WILLY_LEFT": 1,
"PRESENT": 2,
"LADDER": 3,
"TACK": 4,
"UPSPRING": 5,
"SIDESPRING": 6,
"BALL": 7,
"BELL": 8
}
# Add pipe parts
for i in range(1, 41):
namedpart_reverse[f"PIPE{i}"] = 50 + i
namedpart_reverse["BALLPIT"] = 126
namedpart_reverse["EMPTY"] = 127

# Format metadata
metadata = {
"version": 2,
"width": 128,
"height": 128,
"channels": 4, # RGBA
"characters": namedpart_reverse
}

# Initialize output data
header_bytes = json.dumps(metadata).encode('utf-8')
header_size = len(header_bytes)
header_size_bytes = header_size.to_bytes(4, byteorder='little')

# Open output file
with open(output_file, 'wb') as f:
# Write header size and header
f.write(header_size_bytes)
f.write(header_bytes)

# Process each character
max_char_index = max(namedpart_reverse.values()) + 1
for char_index in range(max_char_index):
# Find character name if it exists
char_name = None
for name, idx in namedpart_reverse.items():
if idx == char_index:
char_name = name
break

if char_name and os.path.exists(os.path.join(input_dir, f"{char_name}.png")):
# Load image
img = Image.open(os.path.join(input_dir, f"{char_name}.png"))
img = img.convert('RGBA')
if img.size != (128, 128):
img = img.resize((128, 128), Image.NEAREST)

# Write raw pixel data
f.write(img.tobytes())
else:
# Write empty character (transparent)
empty_data = bytes([0] * (128 * 128 * 4))
f.write(empty_data)

def print_character_info():
"""Print available character names and their indices"""
namedpart = {
0: "WILLY_RIGHT",
1: "WILLY_LEFT",
2: "PRESENT",
3: "LADDER",
4: "TACK",
5: "UPSPRING",
6: "SIDESPRING",
7: "BALL",
8: "BELL"
}
# Add pipe parts 51-90
for i in range(51, 91):
namedpart[i] = f"PIPE{i-50}"
namedpart[126] = "BALLPIT"
namedpart[127] = "EMPTY"

print("\nAvailable characters:")
for idx, name in sorted(namedpart.items()):
print(f"{idx}: {name}")

def read_chr_file(chr_file):
"""
Read a chr file and return the metadata and image data.
Supports both old (8x8) and new (128x128) formats.
"""
with open(chr_file, 'rb') as f:
# Try to read as new format first
try:
header_size = int.from_bytes(f.read(4), byteorder='little')
header_data = f.read(header_size)
metadata = json.loads(header_data.decode('utf-8'))

# It's a new format file
if metadata["version"] == 2:
return metadata, f.read()
except:
# If that fails, assume it's an old format file
f.seek(0)
return None, f.read()

if __name__ == '__main__':
import sys

if len(sys.argv) < 2 or sys.argv[1] not in ['extract', 'create', 'info']:
print("Usage:")
print(" Extract: python willy_chr_tools.py extract <willy.chr> <output_dir>")
print(" Create: python willy_chr_tools.py create <input_dir> <output.chr>")
print(" Info: python willy_chr_tools.py info")
sys.exit(1)

if sys.argv[1] == 'extract':
if len(sys.argv) != 4:
print("Extract usage: python willy_chr_tools.py extract <willy.chr> <output_dir>")
sys.exit(1)
extract_characters(sys.argv[2], sys.argv[3])
elif sys.argv[1] == 'create':
if len(sys.argv) != 4:
print("Create usage: python willy_chr_tools.py create <input_dir> <output.chr>")
sys.exit(1)
create_chr_file(sys.argv[2], sys.argv[3])
else:
print_character_info()
120 changes: 82 additions & 38 deletions willy.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,13 @@ def hq4x_scale(surface):
return new_surface

def loadFont(SCALER, screenfillred=0, screenfillgreen=0, screenfillblue=255):
"""Creates game sprites directly using Pygame surfaces instead of PIL"""
"""Creates game sprites from chr file, handling both old 8x8 and new 128x128 formats.
Maintains game's 8x8 cell grid while displaying high-res sprites."""
import json
from PIL import Image
import io
import numpy as np

namedpart = {
"0": "WILLY_RIGHT", "1": "WILLY_LEFT", "2": "PRESENT", "3": "LADDER",
"4": "TACK", "5": "UPSPRING", "6": "SIDESPRING", "7": "BALL", "8": "BELL"
Expand All @@ -576,14 +582,9 @@ def loadFont(SCALER, screenfillred=0, screenfillgreen=0, screenfillblue=255):
namedpart["126"] = "BALLPIT"
namedpart["127"] = "EMPTY"

# Define colors
BACKGROUND = (screenfillred, screenfillgreen, screenfillblue)
WHITE = (255, 255, 255)

# Create sprite dictionary
char_array = {}

# Read the character data file
# Get the path to chr file
if getattr(sys, 'frozen', False):
__file__ = os.path.dirname(sys.executable)
else:
Expand All @@ -596,40 +597,83 @@ def loadFont(SCALER, screenfillred=0, screenfillgreen=0, screenfillblue=255):
path_to_chr = os.path.abspath(os.path.join(bundle_dir, 'willy.chr'))

with open(path_to_chr, 'rb') as f:
data = bytearray(f.read())

# Process each character
for i in range(len(data) // 8):
char_surface = pygame.Surface((CHAR_WIDTH, CHAR_HEIGHT))
char_surface.fill(BACKGROUND)

bits = [((data[i * 8 + j] >> k) & 1) for j in range(8) for k in range(7, -1, -1)]

for y in range(CHAR_HEIGHT):
for x in range(CHAR_WIDTH):
index = y * CHAR_WIDTH + x
if bits[index] == 1:
char_surface.set_at((x, y), WHITE)

# Apply HQ2x scaling first
scaled_surface = char_surface
iterations = max(1, int(math.log2(SCALER)))
for _ in range(iterations):
scaled_surface = hq4x_scale(scaled_surface)

# Final scaling to exact size if needed
final_size = (int(CHAR_WIDTH * SCALER), int(CHAR_HEIGHT * SCALER))
if scaled_surface.get_size() != final_size:
scaled_surface = pygame.transform.scale(scaled_surface, final_size)

# Try to read as new format first
try:
partnumber = namedpart[str(i)]
char_array[partnumber] = scaled_surface.convert()
except KeyError:
pass
header_size = int.from_bytes(f.read(4), byteorder='little')
header_data = f.read(header_size)
metadata = json.loads(header_data.decode('utf-8'))

return char_array
# New format (128x128 RGBA)
if metadata["version"] == 2:
char_size = metadata["width"] * metadata["height"] * metadata["channels"]
char_data = f.read()

# Calculate final sprite size based on cell size and scaler
cell_size = 8 * SCALER # Size of game grid cell
sprite_scale = cell_size / 128 # Scale to fit in cell

for i in range(128): # Process all possible characters
start = i * char_size
end = start + char_size
if end <= len(char_data):
try:
char_name = namedpart[str(i)]

# Extract and convert pixel data
pixel_data = char_data[start:end]
img = Image.frombytes('RGBA', (128, 128), pixel_data)

# Scale to fit in game grid cell
final_size = (int(cell_size), int(cell_size))
img = img.resize(final_size, Image.NEAREST)

# Convert to Pygame surface
pygame_surface = pygame.image.fromstring(
img.tobytes(), img.size, 'RGBA'
).convert_alpha()

char_array[char_name] = pygame_surface
except KeyError:
continue

return char_array

except Exception as e:
# Fall back to old format
f.seek(0)
data = bytearray(f.read())

# Process each character (old 8x8 format)
for i in range(len(data) // 8):
# Create a new Pygame surface with alpha
char_surface = pygame.Surface((8, 8), pygame.SRCALPHA)

# Extract bits for each row
bits = [((data[i * 8 + j] >> k) & 1) for j in range(8) for k in range(7, -1, -1)]

# Draw pixels on surface
for y in range(8):
for x in range(8):
index = y * 8 + x
if bits[index] == 1:
char_surface.set_at((x, y), (255, 255, 255, 255))
else:
char_surface.set_at((x, y), (0, 0, 0, 0)) # Transparent

# Scale the surface to match game grid
if SCALER != 1:
new_size = (int(8 * SCALER), int(8 * SCALER))
char_surface = pygame.transform.scale(char_surface, new_size)

# Store in character array if it has a name
try:
char_name = namedpart[str(i)]
char_array[char_name] = char_surface.convert_alpha()
except KeyError:
continue

return char_array

def main():
global SCALER
pygame.init()
Expand Down

0 comments on commit 174c85e

Please sign in to comment.