One of the most important features I wanted to include with this game was the ability to load level set data files. As the name implies, a data file represents a set of levels. The reason I wanted to include this is because since the game's release, a community of people have created hundreds of custom level sets that can be found using a web search. These were made by using some custom level editors written by other community members.
The level file can be changed in code.py by altering the DATA_FILE setting. Although this should load most custom levels, some of the ones using more advanced coding techniques that take advantage of glitches in the Microsoft version of the game, though this functionality could probably be added without too much trouble.
To achieve loading the data file dynamically, the file is parsed byte by byte and loaded into a useable data structure for the game. An explanation of the file structure is explained in Chips Challenge File Layout and was the basis of how the dynamic loading took place. If you would like to take a closer look at the code, it is contained in the level.py file.
The load() function reads the binary file byte by byte to extract the necessary data. It starts by validating the file header, finding the starting position of the level data, and then extracting it out to instance variables that can be read by the game logic.
def load(self, level_number):
#pylint: disable=too-many-branches, too-many-locals
# Reset the data prior to loading
self._reset_data()
# Read the file and fill in the variables
with open(self._data_file, "rb") as file:
# Read the first 4 bytes in little endian format
if read_int(file, 4) not in (0x0002AAAC, 0x0102AAAC):
raise ValueError("Not a CHIP file")
self.last_level = read_int(file, 2)
if not 0 < level_number <= self.last_level:
raise ValueError("Invalid level number")
self.level_number = level_number
# Seek to the start of the level data for the specified level
while True:
level_bytes = read_int(file, 2)
if read_int(file, 2) == level_number:
break
# Go to next level
file.seek(level_bytes - 2, 1)
# Read the level data
self.time_limit = read_int(file, 2)
self.chips_required = read_int(file, 2)
compression = read_int(file, 2)
if compression == COMPRESSED:
raise ValueError("Compressed levels not supported")
# Process the top map data
layer_bytes = read_int(file, 2)
map_data = file.read(layer_bytes)
self._process_map_data(map_data, "top")
# Process the bottom map data
layer_bytes = read_int(file, 2)
map_data = file.read(layer_bytes)
self._process_map_data(map_data, "bottom")
remaining_bytes = read_int(file, 2)
while remaining_bytes > 0:
field_type = read_int(file, 1)
field_size = read_int(file, 1)
remaining_bytes -= (2 + field_size)
if field_type == FIELD_TITLE:
self.title = file.read(field_size).decode("utf-8").replace("\x00", "")
elif field_type == FIELD_HINT:
self.hint = file.read(field_size).decode("utf-8").replace("\x00", "")
elif field_type == FIELD_PASSWORD:
self.password = (
"".join([chr(c ^ 0x99) for c in file.read(field_size)]).replace("\x99", "")
)
elif field_type == FIELD_BEAR_TRAPS:
trap_count = field_size // 10
for _ in range(trap_count):
button = Point(read_int(file, 2), read_int(file, 2))
device = Point(read_int(file, 2), read_int(file, 2))
self.traps.append(Device(button, device))
file.seek(2, 1)
elif field_type == FIELD_CLONING_MACHINES:
cloner_count = field_size // 8
for _ in range(cloner_count):
button = Point(read_int(file, 2), read_int(file, 2))
device = Point(read_int(file, 2), read_int(file, 2))
self.cloners.append(Device(button, device))
elif field_type == FIELD_MOVING_CREATURES:
creature_count = field_size // 2
for _ in range(creature_count):
self.creatures.append(Point(
read_int(file, 1),
read_int(file, 1)
))
# Load passwords if not already loaded
if len(self.passwords) == 0:
self._load_passwords(file)
The read_int() function is a helper function which reads a certain number of in little endian format and converts them to an integer. Little endian means a multi-byte value has the least significant bytes written first in the file.
def read_int(file, byte_count):
return int.from_bytes(file.read(byte_count), "little")
At the end of loading the level file, on the first time the file is loaded, the level passwords are also extracted out. This is so the game can check if a particular level password is correct without discarding the current level. This is done by moving the pointer back to the beginning of the file and then going level by level to extract each password and decode it.
def _load_passwords(self, file):
file.seek(6) # Skip the file header
while True:
file.seek(2, 1)
level_number = read_int(file, 2)
file.seek(6, 1)
layer_bytes = read_int(file, 2) # Number of bytes in the top layer
file.seek(layer_bytes, 1) # Skip top layer
layer_bytes = read_int(file, 2) # Number of bytes in the top layer
file.seek(layer_bytes, 1) # Skip bottom layer
remaining_bytes = read_int(file, 2)
while remaining_bytes > 0:
field_type = read_int(file, 1)
field_size = read_int(file, 1)
remaining_bytes -= (2 + field_size)
if field_type == FIELD_PASSWORD:
password = file.read(field_size)
self.passwords[level_number] = (
"".join([chr(c ^ 0x99) for c in password]).replace("\x99", "")
)
file.seek(remaining_bytes, 1)
break
file.seek(field_size, 1)
if len(self.passwords) == self.last_level:
break
Page last edited April 09, 2025
Text editor powered by tinymce.