diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..164b07d --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# === GRAPHIC OPTIONS === + +# - RENDERING OPTIONS - +RAYCASTER_FOV=90 # Field Of View in degrees (°) Keep between 60 and 110 for better rendering +RAYCASTER_NUM_RAYS=200 # Resolution (the lower, the faster) +RAYCASTER_MAX_DISTANCE=500 # For distance shading, + +# - MINIMAP - +MAP_MINIMAP_SIZE=12 +MAP_MINIMAP_OPACITY=210 # range from 0 to 255 + +# === CONTROLS === + +# KEY BINDS +KEY_FORWARD=z +KEY_LEFT=q +KEY_BACKWARD=s +KEY_RIGHT=d + +# === DEV OPTIONS === + +# LOGGER +LOG_LEVEL=INFO \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b02c49a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +temp/ +*.log +env +.env \ No newline at end of file diff --git a/LICENSE b/LICENSE index 3572e28..21b5525 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2020 8KSpaceInvader +Copyright (c) 2025 MxPerrot Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5e15191..2910ec9 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,49 @@ -# A Python raycasting engine made in openGL -This ia a simple python raycasting engine made in python. +# Raycaster2 + +This is a python raycasting game based on 8KSpaceInvader's [pythonRaycaster](https://github.com/ARYANTECH123/pythonRaycaster) + +I've transformed it using OOP to make a multiplayer server/client based game. + +I'm not yet sure what to do as game content, anything is possible from there, i'm thinking of a co-op maze game. + +# Installation + To run it, you must have Python3 and PyOpenGL. Python can be installed from the [website](python.org) To install PyOpenGL, you can go to the [documentaion](http://pyopengl.sourceforge.net/documentation/index.html) -Once both are ready and configured, download or clone `ray.py` and use `python ray.py` to run it. -## Running the code -Once the program starts, you'll see 2 views : A 2D map with the player and all the rays, and a 3d, raycasted projection. Use WASD to move, and Q to teleport. +# Running the game + +Once both are ready and configured, download or clone the project and run the server (`python3 server_network.py`) and then a client (`python3 main.py`). + +Once the program starts, you'll see 2 views : A 2D map with the player, and a 3d, raycasted projection. -## Playing with the code +Use ZQSD to move (I'm using a french azerty keyboard, sorry, I'll later use automatic detection of key layout and/or easy keybindings ui). -The algorithm, though it may appear complex, is actually really simple and fast, and hence was used on many classic games like Wolfenstien 3D, to great effect on the limited hardware of the time. If you want to learn more about the algorithm, [this guide is a excellant starting point](https://permadi.com/1996/05/ray-casting-tutorial-1/#INTRODUCTION). +You can run as many clients as you wish. Other clients will render on the minimap as a green dot. -In order to change the map, simploy edit the 64 element long `world` list. it is formatted so thet a `0` is a space and a `1` is a block. +## About Raycasting + +The algorithm, though it may appear complex, is actually really simple and fast, and hence was used on many classic games like Wolfenstien 3D, to great effect on the limited hardware of the time. + +It consists of shooting rays from the camera until they intersect an object; and then draw a vertical line on the screen depending on the distance reached by the ray. This results + +If you want to learn more about the algorithm, [this guide is a excellent starting point](https://permadi.com/1996/05/ray-casting-tutorial-1/#INTRODUCTION). + +## Future additions + +You can see planned additions in the [TODO](./TODO) file. +Currently I plan on making +- UI for map management/edition and key bindings +- Map generation (maybe mazelik/terrainlike) +- Game mechanics (not yet determined) +- Improve graphics + - Use texture mapping ## Screenshots -[img](https://github.com/ARYANTECH123/pythonRaycaster/blob/main/image.png) +![8kSpaceInvader's old version](https://github.com/ARYANTECH123/pythonRaycaster/blob/main/image.png) + +![My version (IMAGE TO COME)]() diff --git a/TODO b/TODO new file mode 100644 index 0000000..fdd2ed0 --- /dev/null +++ b/TODO @@ -0,0 +1,2 @@ +[ ] Use texture mapping +[ ] Multiplayer: render other players \ No newline at end of file diff --git a/image.png b/image.png deleted file mode 100644 index 65d0f02..0000000 Binary files a/image.png and /dev/null differ diff --git a/logger/CustomFormatter.py b/logger/CustomFormatter.py new file mode 100644 index 0000000..b049632 --- /dev/null +++ b/logger/CustomFormatter.py @@ -0,0 +1,64 @@ +import logging +from logging.handlers import TimedRotatingFileHandler +from dotenv import load_dotenv +import os +import datetime + +load_dotenv() + +LOG_LEVEL = os.getenv('LOG_LEVEL','INFO') +LOG_FOLDER = os.path.join(os.getcwd(), "logs") +os.makedirs(LOG_FOLDER, exist_ok=True) + +class CustomFormatter(logging.Formatter): + cyan = "\x1b[36;20m" + grey = "\x1b[38;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" + + FORMATS = { + logging.DEBUG: cyan + format + reset, + logging.INFO: grey + format + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: bold_red + format + reset + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + +def get_logger(name: str, level=logging.INFO): + logger = logging.getLogger(name) + if not logger.handlers: # Avoid duplicate handlers + + # Console handler (with colors) + console_handler = logging.StreamHandler() + console_handler.setFormatter(CustomFormatter()) + logger.addHandler(console_handler) + + # Get today's date in ISO 8601 format (YYYY-MM-DD) + date_str = datetime.date.today().isoformat() + log_file_path = os.path.join(LOG_FOLDER, f"{date_str}.log") + + # File handler with daily rotation (even if we don't auto-rotate here, keeps it flexible) + file_handler = TimedRotatingFileHandler( + log_file_path, + when="midnight", # Rotate at midnight + interval=1, + backupCount=7, # Keep last 7 logs (optional) + encoding='utf-8' + ) + file_formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + logger.setLevel(level) + + return logger \ No newline at end of file diff --git a/logger/__init__.py b/logger/__init__.py new file mode 100644 index 0000000..263e0f5 --- /dev/null +++ b/logger/__init__.py @@ -0,0 +1 @@ +from .CustomFormatter import get_logger diff --git a/main.py b/main.py new file mode 100644 index 0000000..f708b42 --- /dev/null +++ b/main.py @@ -0,0 +1,161 @@ +############################################################################### +# IMPORTS # +############################################################################### +#from alive_progress import alive_bar +from logger import get_logger +from raycaster import Map, Player, Renderer +from OpenGL.GL import * +from OpenGL.GLU import * +from OpenGL.GLUT import * +from multiplayer import ClientNetwork +import time +import threading +from dotenv import load_dotenv +import os + + +############################################################################### +# CONSTANTS # +############################################################################### + +log = get_logger(__name__) + +log.info("imports successful") + +load_dotenv() + +log.info("environment loaded") + +def get_env_int(key, default): + try: + return int(os.getenv(key, default)) + except ValueError: + return default + +def get_env_str(key, default): + return os.getenv(key, default) # is str by default + +FOV = get_env_int("RAYCASTER_FOV", 80) +NUM_RAYS = get_env_int("RAYCASTER_NUM_RAYS",120) +MAX_DISTANCE = get_env_int("RAYCASTER_MAX_DISTANCE",500) +KEY_FORWARD = get_env_str("KEY_FORWARD",'w') +KEY_LEFT = get_env_str("KEY_LEFT",'a') +KEY_BACKWARD = get_env_str("KEY_BACKWARD",'s') +KEY_RIGHT = get_env_str("KEY_RIGHT",'d') +MAP_MINIMAP_SIZE = get_env_int("MAP_MINIMAP_SIZE",4) +MAP_MINIMAP_OPACITY = get_env_int("MAP_MINIMAP_OPACITY",128) + + +############################################################################### +# INITIALIZATION # +############################################################################### + +# === NETWORK INITIALIZATION === +network = ClientNetwork() # Connects to server at localhost:5555 by default + +# === GAME INITIALIZATION === +# Wait for map data +log.info("Waiting for map...") +while network.map_data is None: + continue +log.info("Received map") + +map_info = network.map_data +map_obj = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info["colorMap"], map_info["spawnpoint"]) + +# You can adjust keybindings per client instance +key_bindings = {'FORWARD': KEY_FORWARD, 'BACKWARD': KEY_BACKWARD, 'LEFT': KEY_LEFT, 'RIGHT': KEY_RIGHT} +player = Player(map_info["spawnpoint"][0], map_info["spawnpoint"][1], 90, key_bindings, map_obj) + +renderer = Renderer(map_obj=map_obj, fov=FOV, num_rays=NUM_RAYS, max_distance=MAX_DISTANCE, minimap_size=MAP_MINIMAP_SIZE, minimap_opacity=MAP_MINIMAP_OPACITY) # Local player only for now + +last_time = time.time() + + +# === GLUT INITIALIZATION === +glutInit() +glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) +glutInitWindowSize(1024, 512) +glutCreateWindow(b"Networked Raycaster Client") + +glEnable(GL_BLEND) +glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + +glutReshapeFunc(renderer.reshape) + + +# === INPUT HANDLERS === +def keyboard_down(key, x, y): + try: + key_char = key.decode().lower() + if key_char in player.key_bindings.values(): + player.keys_pressed.add(key_char) + except UnicodeDecodeError: + pass + +def keyboard_up(key, x, y): + try: + key_char = key.decode().lower() + if key_char in player.keys_pressed: + player.keys_pressed.remove(key_char) + except UnicodeDecodeError: + pass + + +# === DISPLAY FUNCTION === +def display(): + if not network.running: + log.warning("Server disconnected. Exiting client.") + os._exit(0) # Force exit immediately (no need to keep GLUT running) + + global last_time + current_time = time.time() + delta_time = current_time - last_time + last_time = current_time + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + + # Move local player + player.move(delta_time) + + # Send local player update to server + network.send_player_update(player.px, player.py, player.pa) + + # Draw local scene + renderer.draw_scene(player) + + # Draw remote players (optional, if desired) + for remote_id, remote_data in network.players_state.items(): + # Skip rendering ourself + if remote_id == str(network.my_id): # IDs are string keys after JSON + continue + px = int(remote_data['px']) + py = int(remote_data['py']) + pa = int(remote_data['pa']) + renderer.draw_minimap_player((px,py,pa)) + + glutSwapBuffers() + + log.debug(f"Rendering players_state: {network.players_state}") + + +# === GLUT INITIALIZATION PART 2 === + +glClearColor(0.3, 0.3, 0.3, 0) +gluOrtho2D(0, 1024, 512, 0) + +glutKeyboardFunc(keyboard_down) +glutKeyboardUpFunc(keyboard_up) + +glutDisplayFunc(display) +glutIdleFunc(display) + +# === CLEANUP === +def on_close(): + network.close() + +import atexit +atexit.register(on_close) + +# === MAIN LOOP === +glutMainLoop() diff --git a/mapeditor/MapEditor.py b/mapeditor/MapEditor.py new file mode 100644 index 0000000..e1237bc --- /dev/null +++ b/mapeditor/MapEditor.py @@ -0,0 +1,434 @@ +import tkinter as tk +from tkinter import filedialog, messagebox, simpledialog, colorchooser +import json +import os + +def rgb_to_hex(rgb): + """Convert an [R, G, B] list to a hex color.""" + return "#%02x%02x%02x" % tuple(rgb) + +class MapEditor: + def __init__(self, master): + self.master = master + self.master.title("Map Editor") + + # Folder and file info + self.current_folder = None + self.current_file = None + self.map_data = None # Will hold JSON data + self.grid_data = [] # 2D list representation of grid + self.tile_size = 32 # Default tile size + + # Spawnpoint (pixel coordinates) and dragging flag + self.spawnpoint = None + self.dragging_spawnpoint = False + + # Define textures: + # Non-paintable textures (only color adjustable, not used for painting) + self.non_paintable_textures = ["ground", "sky"] + # Paintable textures (appear as selectable radio buttons) + # Now include "0" as the erase texture and "1" as the first painting texture. + self.paintable_textures = ["0", "1"] + self.all_textures = self.non_paintable_textures + self.paintable_textures + + # Default colors. + # "0" (erase) is white, "ground" and "sky" come from your sample, "1" is wall. + self.texture_colors = { + "0": "#ffffff", # erase/void (white) + "ground": "#4ac22c", # from [74, 194, 44] + "sky": "#ebfffe", # from [235, 255, 254] + "1": "#db4900" # default wall + } + + # Selected painting texture (only among the paintable ones) + self.texture_var = tk.StringVar(value="1") + + # Layout: left (file management), center (canvas/controls), right (texture editor) + self.left_frame = tk.Frame(self.master) + self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5) + + self.center_frame = tk.Frame(self.master) + self.center_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.texture_frame = tk.Frame(self.master) + self.texture_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=5, pady=5) + + self.create_widgets() + self.create_texture_sidebar() + + def create_widgets(self): + # --- Left frame: Folder selection, file list, and add new map --- + self.btn_select_folder = tk.Button(self.left_frame, text="Select Folder", command=self.select_folder) + self.btn_select_folder.pack(pady=5) + + self.btn_add_map = tk.Button(self.left_frame, text="Add Map", command=self.add_map) + self.btn_add_map.pack(pady=5) + + self.listbox_files = tk.Listbox(self.left_frame, width=30) + self.listbox_files.pack(fill=tk.BOTH, expand=True) + self.listbox_files.bind("<>", self.on_file_select) + + # --- Center frame: Canvas and controls --- + self.canvas = tk.Canvas(self.center_frame, bg="gray") + self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + self.canvas.bind("", self.canvas_click) + self.canvas.bind("", self.canvas_drag) + self.canvas.bind("", self.canvas_release) + + self.control_frame = tk.Frame(self.center_frame) + self.control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5) + + # Sliders for map width, height, and tile size. + self.width_slider = tk.Scale(self.control_frame, from_=4, to=50, orient=tk.HORIZONTAL, + label="Width", command=self.on_width_change) + self.width_slider.pack(side=tk.LEFT, padx=5) + + self.height_slider = tk.Scale(self.control_frame, from_=4, to=50, orient=tk.HORIZONTAL, + label="Height", command=self.on_height_change) + self.height_slider.pack(side=tk.LEFT, padx=5) + + self.tile_size_slider = tk.Scale(self.control_frame, from_=8, to=64, orient=tk.HORIZONTAL, + label="Tile Size", command=self.on_tile_size_change) + self.tile_size_slider.set(self.tile_size) + self.tile_size_slider.pack(side=tk.LEFT, padx=5) + + # Save button. + self.btn_save = tk.Button(self.control_frame, text="Save", command=self.save_file) + self.btn_save.pack(side=tk.LEFT, padx=5) + + def create_texture_sidebar(self): + """Creates the texture sidebar with color pickers and, for paintable textures, radio buttons.""" + # Clear the texture frame. + for widget in self.texture_frame.winfo_children(): + widget.destroy() + + tk.Label(self.texture_frame, text="Textures", font=("Arial", 12, "bold")).pack(pady=5) + + # For each texture in all_textures, create a row. + for tex in self.all_textures: + frame = tk.Frame(self.texture_frame) + frame.pack(pady=2, padx=5, fill=tk.X) + # For paintable textures (not ground or sky), add a radio button. + if tex not in self.non_paintable_textures: + rb = tk.Radiobutton(frame, text=tex, variable=self.texture_var, value=tex) + rb.pack(side=tk.LEFT) + else: + tk.Label(frame, text=tex).pack(side=tk.LEFT) + # Add a color picker button. + btn = tk.Button(frame, bg=self.texture_colors.get(tex, "gray"), width=6, + command=lambda t=tex: self.choose_color(t)) + btn.pack(side=tk.RIGHT) + + # Button to add a new texture. + add_btn = tk.Button(self.texture_frame, text="Add Texture", command=self.add_texture) + add_btn.pack(pady=10) + + def choose_color(self, tex): + """Let the user choose a new color for the given texture.""" + color = colorchooser.askcolor()[1] + if color: + self.texture_colors[tex] = color + self.draw_grid() + self.create_texture_sidebar() + + def add_texture(self): + """Adds a new paintable texture with the next available number and default gray color.""" + digit_keys = [int(t) for t in self.paintable_textures if t.isdigit()] + next_num = max(digit_keys) + 1 if digit_keys else 1 + next_texture = str(next_num) + self.paintable_textures.append(next_texture) + self.all_textures = self.non_paintable_textures + self.paintable_textures + self.texture_colors[next_texture] = "#808080" # default gray + self.create_texture_sidebar() + + def select_folder(self): + folder = filedialog.askdirectory() + if folder: + self.current_folder = folder + self.populate_file_list() + + def populate_file_list(self): + self.listbox_files.delete(0, tk.END) + for filename in os.listdir(self.current_folder): + if filename.endswith(".json"): + self.listbox_files.insert(tk.END, filename) + + def on_file_select(self, event): + if not self.listbox_files.curselection(): + return + index = self.listbox_files.curselection()[0] + filename = self.listbox_files.get(index) + file_path = os.path.join(self.current_folder, filename) + self.load_file(file_path) + + def load_file(self, file_path): + try: + with open(file_path, "r") as f: + data = json.load(f) + self.current_file = file_path + self.map_data = data + + # Read map dimensions and grid; convert flat list to 2D list. + mapX = data.get("mapX", 0) + mapY = data.get("mapY", 0) + grid = data.get("grid", []) + self.grid_data = [grid[i * mapX:(i + 1) * mapX] for i in range(mapY)] + self.tile_size = data.get("mapS", 32) + self.tile_size_slider.set(self.tile_size) + self.width_slider.set(mapX) + self.height_slider.set(mapY) + + # Update texture colors from the file's colormap. + # Remap keys: "wall" -> "1"; "void"/"erase" -> "0". + if "colorMap" in data: + file_colormap = data["colorMap"] + for key, rgb in file_colormap.items(): + key_str = str(key) + if key_str.lower() == "wall": + key_str = "1" + elif key_str.lower() in ["void", "erase"]: + key_str = "0" + self.texture_colors[key_str] = rgb_to_hex(rgb) + + # Ensure non-paintable textures exist. + defaults = {"ground": "#4ac22c", "sky": "#ebfffe", "1": "#db4900", "0": "#ffffff"} + for tex in self.non_paintable_textures: + if tex not in self.texture_colors: + self.texture_colors[tex] = defaults.get(tex, "#000000") + for tex in ["0", "1"]: + if tex not in self.texture_colors: + self.texture_colors[tex] = defaults.get(tex, "#000000") + + # Determine painting textures: keys that are digits. + self.paintable_textures = sorted( + [key for key in self.texture_colors.keys() + if key not in self.non_paintable_textures and key.isdigit()], + key=lambda x: int(x) + ) + # Make sure "0" and "1" exist. + if "0" not in self.paintable_textures: + self.texture_colors["0"] = self.texture_colors.get("0", "#ffffff") + self.paintable_textures.insert(0, "0") + if "1" not in self.paintable_textures: + self.texture_colors["1"] = self.texture_colors.get("1", "#db4900") + self.paintable_textures.append("1") + + self.all_textures = self.non_paintable_textures + self.paintable_textures + + # Ensure a valid painting texture is selected. + if self.texture_var.get() not in self.paintable_textures: + self.texture_var.set(self.paintable_textures[0]) + + # Set the spawnpoint: if not in file, default to center of the map. + if "spawnpoint" in data: + self.spawnpoint = data["spawnpoint"] + else: + # Default to center: (mapX/2 * tile_size, mapY/2 * tile_size) + self.spawnpoint = [(mapX / 2) * self.tile_size, (mapY / 2) * self.tile_size] + + self.create_texture_sidebar() + self.draw_grid() + except Exception as e: + messagebox.showerror("Error", f"Failed to load file: {e}") + + def draw_grid(self): + self.canvas.delete("all") + if not self.grid_data: + return + rows = len(self.grid_data) + cols = len(self.grid_data[0]) + self.canvas.config(scrollregion=(0, 0, cols * self.tile_size, rows * self.tile_size)) + for i in range(rows): + for j in range(cols): + x1 = j * self.tile_size + y1 = i * self.tile_size + x2 = x1 + self.tile_size + y2 = y1 + self.tile_size + val = str(self.grid_data[i][j]) + color = self.texture_colors.get(val, "gray") + self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, + outline="black", tags=f"cell_{i}_{j}") + # Draw the spawnpoint on top. + self.draw_spawnpoint() + + def draw_spawnpoint(self): + if self.spawnpoint is None: + return + # Remove any old spawnpoint drawing. + self.canvas.delete("spawnpoint") + x, y = self.spawnpoint + cross_size = 5 + self.canvas.create_line(x - cross_size, y, x + cross_size, y, fill="red", width=2, tags="spawnpoint") + self.canvas.create_line(x, y - cross_size, x, y + cross_size, fill="red", width=2, tags="spawnpoint") + + def is_near_spawn(self, event, tol=8): + """Return True if event is within tol pixels of the spawnpoint.""" + if self.spawnpoint is None: + return False + x, y = self.spawnpoint + dx = event.x - x + dy = event.y - y + return (dx * dx + dy * dy) <= (tol * tol) + + def update_spawnpoint(self, event): + """Update spawnpoint to event coordinates and redraw.""" + self.spawnpoint = [event.x, event.y] + self.draw_grid() + + def canvas_click(self, event): + # If click is near the spawnpoint, start dragging it. + if self.is_near_spawn(event): + self.dragging_spawnpoint = True + self.update_spawnpoint(event) + else: + self.paint_at(event) + + def canvas_drag(self, event): + if self.dragging_spawnpoint: + self.update_spawnpoint(event) + else: + self.paint_at(event) + + def canvas_release(self, event): + self.dragging_spawnpoint = False + + def get_cell_from_coords(self, event): + col = event.x // self.tile_size + row = event.y // self.tile_size + if row < 0 or row >= len(self.grid_data) or col < 0 or col >= len(self.grid_data[0]): + return None, None + return row, col + + def paint_at(self, event): + row, col = self.get_cell_from_coords(event) + if row is not None and col is not None and self.texture_var.get(): + new_val = int(self.texture_var.get()) + if self.grid_data[row][col] != new_val: + self.grid_data[row][col] = new_val + x1 = col * self.tile_size + y1 = row * self.tile_size + x2 = x1 + self.tile_size + y2 = y1 + self.tile_size + color = self.texture_colors.get(str(new_val), "gray") + self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, + outline="black", tags=f"cell_{row}_{col}") + + def on_width_change(self, value): + if not self.grid_data: + return + new_width = int(value) + old_width = len(self.grid_data[0]) + rows = len(self.grid_data) + for i in range(rows): + if new_width > old_width: + self.grid_data[i].extend([0] * (new_width - old_width)) + elif new_width < old_width: + self.grid_data[i] = self.grid_data[i][:new_width] + if self.map_data is not None: + self.map_data["mapX"] = new_width + self.draw_grid() + + def on_height_change(self, value): + if not self.grid_data: + return + new_height = int(value) + old_height = len(self.grid_data) + cols = len(self.grid_data[0]) + if new_height > old_height: + for _ in range(new_height - old_height): + self.grid_data.append([0] * cols) + elif new_height < old_height: + self.grid_data = self.grid_data[:new_height] + if self.map_data is not None: + self.map_data["mapY"] = new_height + self.draw_grid() + + def on_tile_size_change(self, value): + self.tile_size = int(value) + if self.map_data is not None: + self.map_data["mapS"] = self.tile_size + self.draw_grid() + + def color_to_hex(self, color): + """Convert a color name or hex string to a hex string.""" + if color.startswith("#"): + return color + else: + r, g, b = self.master.winfo_rgb(color) + r = int(r / 256) + g = int(g / 256) + b = int(b / 256) + return "#{:02x}{:02x}{:02x}".format(r, g, b) + + def save_file(self): + if not self.current_file or not self.map_data: + return + # Flatten the 2D grid back to a flat list. + flat_grid = [cell for row in self.grid_data for cell in row] + self.map_data["grid"] = flat_grid + self.map_data["mapX"] = len(self.grid_data[0]) + self.map_data["mapY"] = len(self.grid_data) + self.map_data["mapS"] = self.tile_size + # Save the spawnpoint. + if self.spawnpoint is not None: + self.map_data["spawnpoint"] = self.spawnpoint + + # Update the colormap for all textures. + new_colorMap = {} + for tex in self.all_textures: + hex_color = self.texture_colors.get(tex, "#000000") + hex_color = self.color_to_hex(hex_color) + hex_color = hex_color.lstrip("#") + rgb = [int(hex_color[i:i+2], 16) for i in (0, 2, 4)] + new_colorMap[tex] = rgb + self.map_data["colorMap"] = new_colorMap + + try: + with open(self.current_file, "w") as f: + json.dump(self.map_data, f, indent=4) + messagebox.showinfo("Saved", "Map saved successfully!") + except Exception as e: + messagebox.showerror("Error", f"Failed to save file: {e}") + + def add_map(self): + if not self.current_folder: + messagebox.showwarning("No Folder Selected", "Please select a folder first.") + return + filename = simpledialog.askstring("New Map", "Enter new map filename (with .json extension):") + if not filename: + return + if not filename.endswith(".json"): + filename += ".json" + file_path = os.path.join(self.current_folder, filename) + if os.path.exists(file_path): + messagebox.showwarning("File Exists", "File already exists!") + return + # Default map: 16x16 grid, tile size 32, default colormap, and spawnpoint at center. + default_map = { + "grid": [0] * (16 * 16), + "mapX": 16, + "mapY": 16, + "mapS": 32, + "spawnpoint": [(16 / 2) * 32, (16 / 2) * 32], + "colorMap": { + "ground": [74, 194, 44], + "sky": [235, 255, 254], + "1": [219, 73, 0], + "0": [255, 255, 255] + } + } + try: + with open(file_path, "w") as f: + json.dump(default_map, f, indent=4) + messagebox.showinfo("Map Created", f"Created new map: {filename}") + self.populate_file_list() + except Exception as e: + messagebox.showerror("Error", f"Failed to create file: {e}") + +def main(): + root = tk.Tk() + editor = MapEditor(root) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/mapeditor/__init__.py b/mapeditor/__init__.py new file mode 100644 index 0000000..b8df6a7 --- /dev/null +++ b/mapeditor/__init__.py @@ -0,0 +1 @@ +from .MapEditor import MapEditor \ No newline at end of file diff --git a/maps/aryantech123.json b/maps/aryantech123.json new file mode 100644 index 0000000..b2408df --- /dev/null +++ b/maps/aryantech123.json @@ -0,0 +1,97 @@ +{ + "grid": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1 + ], + "mapX": 8, + "mapY": 8, + "mapS": 64, + "colorMap": { + "ground": [ + 74, + 194, + 44 + ], + "sky": [ + 235, + 255, + 254 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 219, + 73, + 0 + ] + }, + "spawnpoint": [ + 256.0, + 256.0 + ] +} \ No newline at end of file diff --git a/maps/empty_l.json b/maps/empty_l.json new file mode 100644 index 0000000..e56d4b7 --- /dev/null +++ b/maps/empty_l.json @@ -0,0 +1,289 @@ +{ + "grid": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "mapX": 16, + "mapY": 16, + "mapS": 64, + "colorMap": { + "ground": [ + 74, + 194, + 44 + ], + "sky": [ + 235, + 255, + 254 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 219, + 73, + 0 + ] + }, + "spawnpoint": [ + 512.0, + 512.0 + ] +} \ No newline at end of file diff --git a/maps/empty_xl.json b/maps/empty_xl.json new file mode 100644 index 0000000..4b689d2 --- /dev/null +++ b/maps/empty_xl.json @@ -0,0 +1,2538 @@ +{ + "grid": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "mapX": 50, + "mapY": 50, + "mapS": 32, + "spawnpoint": [ + 83, + 176 + ], + "colorMap": { + "ground": [ + 237, + 241, + 163 + ], + "sky": [ + 235, + 255, + 254 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 198, + 163, + 55 + ], + "2": [ + 45, + 210, + 69 + ] + } +} \ No newline at end of file diff --git a/maps/house_l.json b/maps/house_l.json new file mode 100644 index 0000000..1f0fa6d --- /dev/null +++ b/maps/house_l.json @@ -0,0 +1,294 @@ +{ + "grid": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "mapX": 16, + "mapY": 16, + "mapS": 32, + "colorMap": { + "ground": [ + 74, + 194, + 44 + ], + "sky": [ + 235, + 255, + 254 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 219, + 73, + 0 + ], + "2": [ + 45, + 210, + 69 + ] + }, + "spawnpoint": [ + 256.0, + 256.0 + ] +} \ No newline at end of file diff --git a/maps/mansion.json b/maps/mansion.json new file mode 100644 index 0000000..e853ec7 --- /dev/null +++ b/maps/mansion.json @@ -0,0 +1,294 @@ +{ + "grid": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 2, + 2, + 0, + 0, + 0, + 0, + 2, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 2, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 2, + 2, + 2, + 0, + 0, + 0, + 0, + 2, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0 + ], + "mapX": 16, + "mapY": 16, + "mapS": 32, + "colorMap": { + "ground": [ + 160, + 116, + 71 + ], + "sky": [ + 224, + 232, + 132 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 219, + 73, + 0 + ], + "2": [ + 45, + 210, + 69 + ] + }, + "spawnpoint": [ + 305, + 432 + ] +} \ No newline at end of file diff --git a/maps/map.json b/maps/map.json new file mode 100644 index 0000000..a96e377 --- /dev/null +++ b/maps/map.json @@ -0,0 +1,102 @@ +{ + "grid": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1 + ], + "mapX": 8, + "mapY": 8, + "mapS": 64, + "colorMap": { + "ground": [ + 74, + 194, + 44 + ], + "sky": [ + 235, + 255, + 254 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 219, + 73, + 0 + ], + "2": [ + 45, + 210, + 69 + ] + }, + "spawnpoint": [ + 256.0, + 256.0 + ] +} \ No newline at end of file diff --git a/maps/maze.json b/maps/maze.json new file mode 100644 index 0000000..c42f5a7 --- /dev/null +++ b/maps/maze.json @@ -0,0 +1,438 @@ +{ + "grid": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "mapX": 20, + "mapY": 20, + "mapS": 64, + "spawnpoint": [ + 28, + 92 + ], + "colorMap": { + "ground": [ + 74, + 194, + 44 + ], + "sky": [ + 235, + 255, + 254 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 122, + 122, + 122 + ], + "2": [ + 235, + 241, + 3 + ] + } +} \ No newline at end of file diff --git a/maps/maze2.json b/maps/maze2.json new file mode 100644 index 0000000..10ff1f1 --- /dev/null +++ b/maps/maze2.json @@ -0,0 +1,438 @@ +{ + "grid": [ + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 2, + 2, + 2, + 2, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 2, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 2, + 2, + 2, + 2, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 2, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2 + ], + "mapX": 20, + "mapY": 20, + "mapS": 64, + "spawnpoint": [ + 570, + 737 + ], + "colorMap": { + "ground": [ + 74, + 194, + 44 + ], + "sky": [ + 235, + 255, + 254 + ], + "0": [ + 255, + 255, + 255 + ], + "1": [ + 0, + 128, + 0 + ], + "2": [ + 104, + 62, + 23 + ] + } +} \ No newline at end of file diff --git a/maps/test.json b/maps/test.json new file mode 100644 index 0000000..f5d8d29 --- /dev/null +++ b/maps/test.json @@ -0,0 +1,230 @@ +{ + "grid": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "mapX": 48, + "mapY": 4, + "mapS": 32, + "colorMap": { + "ground": [ + 58, + 58, + 58 + ], + "sky": [ + 220, + 230, + 153 + ], + "0": [ + 0, + 0, + 0 + ], + "1": [ + 228, + 92, + 96 + ], + "2": [ + 104, + 62, + 23 + ] + }, + "spawnpoint": [ + 768.0, + 64.0 + ] +} \ No newline at end of file diff --git a/multiplayer/ClientNetwork.py b/multiplayer/ClientNetwork.py new file mode 100644 index 0000000..1c5be05 --- /dev/null +++ b/multiplayer/ClientNetwork.py @@ -0,0 +1,96 @@ +import socket +import threading +import json +import struct +from logger import get_logger + +log = get_logger(__name__) + +class ClientNetwork: + def __init__(self, host='127.0.0.1', port=5555): + self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server.connect((host, port)) + self.players_state = {} + self.my_id = None + self.running = True + self.map_data = None + + + # Start listening + thread = threading.Thread(target=self.listen, daemon=True) + thread.start() + + def recv_exact(self, sock, n): + data = b'' + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data += packet + return data + + def listen(self): + try: + while self.running: + raw_len = self.recv_exact(self.server, 4) + if not raw_len: + log.warning("Server disconnected (length header missing). Closing client.") + break # Stop loop + + msg_len = struct.unpack('!I', raw_len)[0] + data = self.recv_exact(self.server, msg_len) + if not data: + log.warning("Server disconnected (message body missing). Closing client.") + break + + message = json.loads(data.decode()) + + # Handle messages: + if 'init_id' in message: + self.my_id = message['init_id'] + log.info(f"Assigned Player ID: {self.my_id}") + + # Send ACK + try: + ack = json.dumps({"ack": True}).encode() + length = struct.pack('!I', len(ack)) + self.server.sendall(length + ack) + log.info("Sent ACK to server") + except Exception as e: + log.error(f"Error sending ACK: {e}") + break # Connection issue, break out + + elif 'map_data' in message: + self.map_data = message['map_data'] + log.info(f"Received map data") + + else: + self.players_state = message + log.debug(f"Updated players_state: {self.players_state}") + + except Exception as e: + log.critical(f"Listen error: {e}") + finally: + log.warning("Shutting down client listener.") + self.running = False + try: + self.server.close() + except: + pass + + def send_player_update(self, px, py, pa): + if not self.running: + return # Don't attempt to send if disconnected + try: + message = {"px": px, "py": py, "pa": pa} + message_bytes = json.dumps(message).encode() + length = struct.pack('!I', len(message_bytes)) + self.server.sendall(length + message_bytes) + except Exception as e: + log.critical(f"Send failed (server down?): {e}") + self.running = False + + + def close(self): + self.running = False + self.server.close() diff --git a/multiplayer/__init__.py b/multiplayer/__init__.py new file mode 100644 index 0000000..a1b3a18 --- /dev/null +++ b/multiplayer/__init__.py @@ -0,0 +1 @@ +from .ClientNetwork import ClientNetwork \ No newline at end of file diff --git a/ray.py b/ray.py deleted file mode 100644 index ce533ce..0000000 --- a/ray.py +++ /dev/null @@ -1,245 +0,0 @@ -import tkinter -from OpenGL.GL import * -from OpenGL.GLU import * -from OpenGL.GLUT import * - -from math import radians, sin, cos, tan, sqrt - -''' - -Raycasting engine, written in python using OpenGl, GLU, and GLUT - - -''' - -# Accesory function -def FixAng(angle): - a = angle - if(angle>359): - a -= 360 - elif(angle<0): - a += 360 - return a - -# The map that player exists in -world = [ - 1,1,1,1,1,1,1,1, - 1,1,0,1,0,0,0,1, - 1,0,0,0,0,1,0,1, - 1,1,1,0,0,0,0,1, - 1,0,0,0,0,0,0,1, - 1,0,0,0,0,0,0,1, - 1,0,0,0,0,0,0,1, - 1,0,0,0,0,0,0,1 -] - -# Map variables -mapS = 64 -mapX = 8 -mapY = 8 - -# Draws 2D map -def drawMap2d(): - - for y in range(0, mapY): - for x in range(0 , mapX): - if(world[y*mapY+x] == 1): - glColor(1, 1, 1) - else: - glColor(0, 0, 0) - xo, yo = x*mapS, y*mapS - glBegin(GL_QUADS) - glVertex(xo+1, yo+1) - glVertex(xo+1, mapS+yo+1) - glVertex(mapS+xo-1, mapS+yo-1) - glVertex(mapS+xo-1, yo+1) - glEnd() - -# Player position variables -px, py, pa, pdx, pdy = 0,0,0,0,0 - -# Draws 2D player -def drawPlayer2d(px, py, pa, pdx, pdy): - glColor(1, 1, 0) - glPointSize(8) - glLineWidth(4) - - glBegin(GL_POINTS) - glVertex(px, py) - glEnd() - - glBegin(GL_LINES) - glVertex(px, py) - glVertex(px+20*pdx, py+20*pdy) - glEnd() - -# Handles keyboard input callbacks -def buttons(key, x, y): - global px, py, pa, pdx, pdy - if(ord(key) == ord('w')): - px += pdx*5 - py += pdy*5 - elif(ord(key) == ord('a')): - pa += 5 - pa = FixAng(pa) - pdx=cos(radians(pa)) - pdy=-sin(radians(pa)) - elif(ord(key) == ord('d')): - pa -= 5 - pa = FixAng(pa) - pdx=cos(radians(pa)) - pdy=-sin(radians(pa)) - elif(ord(key) == ord('s')): - px -= pdx*5 - py -= pdy*5 - elif(ord(key) == ord('q')): - px = x - py = y - glutPostRedisplay() - -# Drawing all the rays -def drawRays2d(): - # Draws sky - glColor3f(0,1,1) - glBegin(GL_QUADS) - glVertex(526, 0) - glVertex(1006, 0) - glVertex(1006,160) - glVertex(526,160) - glEnd() - - #Draws floor - glColor3f(0,0,1) - glBegin(GL_QUADS) - glVertex2i(526,160) - glVertex2i(1006,160) - glVertex2i(1006,320) - glVertex2i(526,320) - glEnd() - - #ra is the ray angle - ra = FixAng(pa + 30) - - for r in range(1, 60): # We are drawing total 60 rays, for a 60 degree field of view - - # Checking vertical wall intercept - dof, side, disV = 0, 0, 10000 - - Tan = tan(radians(ra)) - if(cos(radians(ra)) > 0.001): # Looking leftwards - rx = ((int(px) >> 6) << 6) + 64 # First x-intercept - ry = (px - rx)*Tan+py - xo = 64 - yo = -xo * Tan - elif(cos(radians(ra)) < -0.001): # Looking rightwards - rx = ((int(px) >> 6) << 6) - 0.001 - ry = (px - rx)*Tan+py - xo = -64 - yo = -xo * Tan - else: # No vertical hit - rx=px - ry=py - dof=8 - while(dof < 8): - mx = int(rx) >> 6 - my = int(ry) >> 6 - mp = my*mapX + mx - if(mp > 0 and mp < mapX*mapY and world[mp] == 1): # Is the intercept a wall? - dof = 8 - # disV = cos(radians(ra))*(rx-px)-sin(radians(ra))*(ry-py) - disV = sqrt((px-rx)**2 + (py-ry)**2) # Finding vertical distance - else: # Else, check next intercept - rx += xo - ry += yo - dof += 1 - vx = rx - vy = ry - - # Checking Horizontal wall intercept - dof, disH, Tan = 0, 10000, 1/Tan - if(sin(radians(ra)) > 0.001): # Looking up - ry = ((int(py) >> 6) << 6) - 0.0001 - rx = (py-ry)*Tan+px - yo = -64 - xo = -yo*Tan - elif(radians(ra) < -0.001): # Looking down - ry = ((int(py) >> 6) << 6) + 64 - rx = (py-ry)*Tan+px - yo = 64 - xo = -yo*Tan - - while(dof < 8): - mx = int(rx) >> 6 - my = int(ry) >> 6 - mp = my*mapX + mx - if(mp > 0 and mp < mapX*mapY and world[mp] == 1): # Is intercept a wall? - dof = 8 - # disH = cos(radians(ra)) * (rx-px) - sin(radians(ra))*(ry-py) - disH = sqrt((px-rx)**2 + (py-ry)**2) - else: # Now check next intercept - rx += xo - ry += yo - dof += 1 - hx, hy = rx, ry - - if(disV < disH): # If a Vertical wall is hit first - # print("yes") - rx, ry = vx, vy - disH = disV - else: - # print("no") - rx, ry = hx, hy - - # Drawing 2D rays - glColor(0, 0.6, 0) - glLineWidth(2) - glBegin(GL_LINES) - glVertex(px, py) - glVertex(rx, ry) - glEnd() - - # Drawing 3D scene - ca = FixAng(pa - ra) # This is to correct for Fisheye effect, which looks unnatural - disH = disH*cos(radians(ca)) - lineH = mapS*320/disH - if(lineH > 320): - lineH = 320 - lineOff = 160-(lineH // 2) - - glLineWidth(9) - glBegin(GL_LINES) - glVertex(r*8+530,lineOff) - glVertex(r*8+530,lineOff+lineH) - glEnd() - # Looping to next ray - ra = FixAng(ra -1) - -# Initializing basic window parameters -def init(): - global px, py, pa, pdx, pdy - glClearColor(0.3,0.3,0.3,0) - gluOrtho2D(0,1024,510,0) - px=150; py=400; pa=90.1 - pdx=cos(radians(pa)) - pdy=-sin(radians(pa)) - -# Display callback function -def display(): - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - drawMap2d() - drawPlayer2d(px, py, pa, pdx, pdy) - drawRays2d() - glutSwapBuffers() - -# Defining all callbacks and windows. -glutInit() -glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) -glutInitWindowSize(1024, 510) -glutCreateWindow("pyopengl raycater") -init() -glutDisplayFunc(display) -glutIdleFunc(display) -glutKeyboardFunc(buttons) - -glutMainLoop() - diff --git a/raycaster/Map.py b/raycaster/Map.py new file mode 100644 index 0000000..ef8696e --- /dev/null +++ b/raycaster/Map.py @@ -0,0 +1,82 @@ +from OpenGL.GL import * +import json +from logger import get_logger + +log = get_logger(__name__) + +class Map: + def __init__(self, grid, mapX, mapY, mapS, colorMap, spawnpoint): + self.grid = grid + self.mapX = mapX # Map width + self.mapY = mapY # Map height + self.mapS = mapS # Tile size in pixels. Should be 32 to 128. Ideally 64 + self.colorMap = colorMap + self.spawnpoint = spawnpoint + + def is_wall(self, mx, my): + if 0 <= mx < self.mapX and 0 <= my < self.mapY: + return self.grid[my * self.mapX + mx] != 0 + return True # Out of bounds = wall + + # TODO look into if i should use static method here and on other classes + def save_to_file(self, filename): + map_data = { + "grid": self.grid, + "mapX": self.mapX, + "mapY": self.mapY, + "mapS": self.mapS, + "colorMap": self.colorMap, + "spawnpoint": self.spawnpoint + } + with open(filename, 'w') as f: + json.dump(map_data, f) + load.info(f"Saved map to {filename}") + + @classmethod + def load_from_file(cls, filename): + with open(filename, 'r') as f: + map_data = json.load(f) + log.info(f"Loaded map from {filename}") + return cls(map_data["grid"], map_data["mapX"], map_data["mapY"], map_data["mapS"], map_data["colorMap"], map_data["spawnpoint"]) + + def map_to_dict(self): + return { + "grid": self.grid, + "mapX": self.mapX, + "mapY": self.mapY, + "mapS": self.mapS, + "colorMap": self.colorMap, + "spawnpoint": self.spawnpoint + } + + # GETTERS + def get_color(self, texture): + try: + color = self.colorMap[texture] + except: + color = (255,0,255) # MAGENTA FOR ERROR + log.error(f"TEXTURE {texture} DOES NOT EXIST") + return color + + def get_spawnpoint(self): + return self.spawnpoint + + def get_grid(self): + return self.grid + + def get_mapX(self): + return self.mapX + + def get_mapY(self): + return self.mapY + + def get_mapS(self): + return self.mapS + + def get_colorMap(self): + return self.colorMap + + def get_spawnpoint(self): + return self.spawnpoint + + \ No newline at end of file diff --git a/raycaster/Player.py b/raycaster/Player.py new file mode 100644 index 0000000..98a7f62 --- /dev/null +++ b/raycaster/Player.py @@ -0,0 +1,74 @@ +from OpenGL.GL import * +from math import sin, cos, tan, sqrt, radians + +class Player: + def __init__(self, x, y, angle, key_bindings, map_obj): + self.px = x + self.py = y + self.pa = angle + self.pdx = cos(radians(self.pa)) + self.pdy = -sin(radians(self.pa)) + self.keys_pressed = set() + self.key_bindings = key_bindings + self.map = map_obj + + def update_direction(self): + self.pdx = cos(radians(self.pa)) + self.pdy = -sin(radians(self.pa)) + + def move(self, delta_time): + speed = 200 # Pixels per second (adjust as needed) + + move_step = speed * delta_time + + # Rotation + if self.key_bindings['LEFT'] in self.keys_pressed: + self.pa += 360 * delta_time # Degrees per second + self.pa %= 360 + self.update_direction() + if self.key_bindings['RIGHT'] in self.keys_pressed: + self.pa -= 360 * delta_time + self.pa %= 360 + self.update_direction() + + # Movement + new_x = self.px + new_y = self.py + if self.key_bindings['FORWARD'] in self.keys_pressed: + new_x += self.pdx * move_step + new_y += self.pdy * move_step + if self.key_bindings['BACKWARD'] in self.keys_pressed: + new_x -= self.pdx * move_step + new_y -= self.pdy * move_step + + # Collision detection + if not self.map.is_wall(int(new_x) // self.map.mapS, int(new_y) // self.map.mapS): + self.px = new_x + self.py = new_y + + # Getters + def get_px(self): + return self.px + + def get_py(self): + return self.py + + def get_pa(self): + return self.pa + + def get_pdx(self): + return self.pdx + + def get_pdy(self): + return self.pdy + + def get_keys_pressed(self): + return self.keys_pressed + + def get_key_bindings(self): + return self.key_bindings + + def get_map(self): + return self.map + + \ No newline at end of file diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py new file mode 100644 index 0000000..6c1df38 --- /dev/null +++ b/raycaster/Renderer.py @@ -0,0 +1,221 @@ +from math import sin, cos, radians, hypot +from typing import Union, Tuple +from .Player import Player +from OpenGL.GL import * +from OpenGL.GLU import * +from OpenGL.GLUT import * + +class Renderer: + def __init__(self, map_obj, fov:int = 80, num_rays:int = 120, max_distance:int = 500, minimap_size:int = 4, minimap_opacity:int = 128): + self.map = map_obj + self.FOV = fov + self.num_rays = num_rays # resolution + self.max_distance = max_distance # shading + self.minimap_size = minimap_size + self.minimap_opacity = minimap_opacity + + def reshape(self, width, height): + """Handles window resizing correctly.""" + glViewport(0, 0, width, height) # Use full window area + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluOrtho2D(0, width, height, 0) # Top-left origin + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + + def get_window_size(self): + """Dynamically get current window size.""" + width = glutGet(GLUT_WINDOW_WIDTH) + height = glutGet(GLUT_WINDOW_HEIGHT) + return width, height + + def draw_scene(self, player:Player): + self.cast_rays(player) + self.draw_minimap() + self.draw_minimap_player(player) + + def cast_rays(self, player:Player): + + screen_width, screen_height = self.get_window_size() + slice_width = screen_width / self.num_rays + + # Draw sky + sky_color = self.map.get_color("sky") + glColor3ub(*sky_color) + glBegin(GL_QUADS) + glVertex2i(0, 0) + glVertex2i(screen_width, 0) + glVertex2i(screen_width, screen_height // 2) + glVertex2i(0, screen_height // 2) + glEnd() + + # Draw floor + ground_color = self.map.get_color("ground") + glColor3ub(*ground_color) + glBegin(GL_QUADS) + glVertex2i(0, screen_height // 2) + glVertex2i(screen_width, screen_height // 2) + glVertex2i(screen_width, screen_height) + glVertex2i(0, screen_height) + glEnd() + + ra = player.pa + (self.FOV / 2) + for r in range(self.num_rays): + ray_angle = radians(ra) + dof = 0 + dis = 0 + rx, ry = player.px, player.py + hit_wall = False + + while dof < 20: + prev_mx = int(rx) // self.map.get_mapS() + prev_my = int(ry) // self.map.get_mapS() + + rx += cos(ray_angle) * 5 + ry -= sin(ray_angle) * 5 + dof += 0.1 + + mx = int(rx) // self.map.get_mapS() + my = int(ry) // self.map.get_mapS() + + if mx < 0 or mx >= self.map.get_mapX() or my < 0 or my >= self.map.get_mapY(): + break + + hit_cell_value = self.map.get_grid()[my * self.map.get_mapX() + mx] + + if hit_cell_value != 0: + dis = hypot(rx - player.px, ry - player.py) + hit_wall = True + + delta_mx = mx - prev_mx + delta_my = my - prev_my + + if abs(delta_mx) > abs(delta_my): + wall_face = "West" if delta_mx > 0 else "East" + else: + wall_face = "North" if delta_my > 0 else "South" + break + + if not hit_wall: + ra -= (self.FOV / self.num_rays) + continue + + # Fisheye correction + ca = radians(player.pa - ra) + dis *= cos(ca) + + # Wall projection height + line_height = (self.map.mapS * screen_height) / (dis + 0.0001) + if line_height > screen_height: + line_height = screen_height + line_offset = (screen_height // 2) - line_height / 2 + + # Wall color and shading + base_color = self.map.get_color(str(hit_cell_value)) + distance_factor = max(0.7, 1 - (dis / self.get_max_distance()) ** 0.5) + + shade_factors = { + "North": 1.0, + "South": 0.8, + "East": 0.7, + "West": 0.6 + } + face_factor = shade_factors.get(wall_face, 1.0) + final_factor = face_factor * distance_factor + + shaded_color = tuple( + max(0, min(255, int(c * final_factor))) + for c in base_color + ) + + # Draw wall slice: + if r == self.num_rays - 1: + # Special case: make sure last ray hits right of the screen + x = screen_width - 1 + glLineWidth(1) # Single pixel width + else: + x = int(r * slice_width) + glLineWidth(int(slice_width) + 1) + + glColor3ub(*shaded_color) + glBegin(GL_LINES) + glVertex2i(x, int(line_offset)) + glVertex2i(x, int(line_offset + line_height)) + glEnd() + + ra -= (self.FOV / self.num_rays) + + def draw_minimap(self): + minimap_size = self.get_minimap_size() + minimap_opacity = self.get_minimap_opacity() + for y in range(self.map.get_mapY()): + for x in range(self.map.get_mapX()): + cell_value = self.map.get_grid()[y * self.map.get_mapX() + x] + if cell_value != 0 : # if not void + glColor4ub(*self.map.get_color(str(cell_value)),minimap_opacity) + else: + glColor4ub(*self.map.get_color(str("ground")),minimap_opacity) + + xo, yo = x * minimap_size, y * minimap_size + glBegin(GL_QUADS) + glVertex2i(xo, yo ) + glVertex2i(xo, yo + minimap_size) + glVertex2i(xo + minimap_size, yo + minimap_size) + glVertex2i(xo + minimap_size, yo ) + glEnd() + + def draw_minimap_player(self, player_or_coords: Union[Player, Tuple[float, float, float]]) -> None: + + mapS = self.get_map().get_mapS() + minimap_size = self.get_minimap_size() + minimap_opacity = self.get_minimap_opacity() + segment_length = 10 + + if isinstance(player_or_coords, Player): + glColor4ub(0,255,255,minimap_opacity) # CYAN COLOR FOR PLAYER + px, py, pdx, pdy = player_or_coords.get_px(), player_or_coords.get_py(), player_or_coords.get_pdx(), player_or_coords.get_pdy() + else: + glColor4ub(255, 0, 0, minimap_opacity) # RED COLOR FOR OTHER PLAYERS + px, py, pa = player_or_coords # Expecting tuple (px, py, pa) + # Calculate pdx, pdy based on pa: + pdx = cos(radians(pa)) + pdy = -sin(radians(pa)) + + + + + map_x = px / mapS * minimap_size + map_y = py / mapS * minimap_size + + # Dot + glPointSize(8) + glLineWidth(3) + glBegin(GL_POINTS) + glVertex2i(int(map_x), int(map_y)) + glEnd() + + # Forward segment + glBegin(GL_LINES) + glVertex2i(int(map_x), int(map_y)) + glVertex2i(int(map_x + segment_length * pdx), int(map_y + segment_length * pdy)) + glEnd() + + # Getters + + def get_map(self): + return self.map + + def get_FOV(self): + return self.FOV + + def get_num_rays(self): + return self.num_rays + + def get_max_distance(self): + return self.max_distance + + def get_minimap_size(self): + return self.minimap_size + + def get_minimap_opacity(self): + return self.minimap_opacity \ No newline at end of file diff --git a/raycaster/__init__.py b/raycaster/__init__.py new file mode 100644 index 0000000..61243ec --- /dev/null +++ b/raycaster/__init__.py @@ -0,0 +1,3 @@ +from .Map import Map +from .Player import Player +from .Renderer import Renderer diff --git a/server_network.py b/server_network.py new file mode 100644 index 0000000..bf1de76 --- /dev/null +++ b/server_network.py @@ -0,0 +1,193 @@ +import socket +import threading +import json +import struct +import sys +import os +import signal +from logger import get_logger +from raycaster import Map + +log = get_logger(__name__) + +clients = {} # conn: player_id +players_state = {} # player_id: {px, py, pa} +shutdown_event = threading.Event() + + +# === Helper functions === + +def send_message(conn, message_dict): + message = json.dumps(message_dict).encode() + length = struct.pack('!I', len(message)) + conn.sendall(length + message) + + +def recv_exact(sock, n): + data = b'' + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data += packet + return data + + +def broadcast(): + log.debug(f"Broadcasting: {players_state}") + for conn in list(clients.keys()): + try: + send_message(conn, players_state) + except Exception as e: + log.error(f"Broadcast error: {e}") + conn.close() + clients.pop(conn, None) + + +def handle_client(conn, addr, player_id, map_data): + log.info(f"New connection from {addr}") + clients[conn] = player_id + + try: + # Send init_id + send_message(conn, {"init_id": player_id}) + log.info(f"Sent init_id to {addr}") + + # Send map data + send_message(conn, {"map_data": map_data}) + log.info(f"Sent map_data to {addr}") + + # Wait for ACK before broadcasting + raw_len = recv_exact(conn, 4) + if not raw_len: + log.info(f"No ACK received from {addr}") + return + msg_len = struct.unpack('!I', raw_len)[0] + data = recv_exact(conn, msg_len) + try: + ack_msg = json.loads(data.decode()) + except: + ack_msg = {} + log.warning(f"No valid ACK from {addr}") + if 'ack' not in ack_msg: + log.warning(f"Invalid ACK from {addr}") + return + log.info(f"Received ACK from {addr}") + + broadcast() + + # Main receive loop + while not shutdown_event.is_set(): + raw_len = recv_exact(conn, 4) + if not raw_len: + break + msg_len = struct.unpack('!I', raw_len)[0] + data = recv_exact(conn, msg_len) + message = json.loads(data.decode()) + players_state[player_id] = message + + broadcast() + + except ConnectionAbortedError: + log.info(f"Connection aborted: {addr}") + except ConnectionResetError: + log.info(f"Connexion reset by client {addr}") + except Exception as e: + log.warning(f"Client error: {e}") + finally: + log.info(f"Connection lost: {addr}") + conn.close() + clients.pop(conn, None) + players_state.pop(player_id, None) + broadcast() + + +def choose_map(): + log.info("Choosing map...") + MAPS_DIRECTORY = 'maps/' + + # List all files + files = [f for f in os.listdir(MAPS_DIRECTORY) if os.path.isfile(os.path.join(MAPS_DIRECTORY, f))] + + if not files: + print("No map files found!") + sys.exit(1) + + print("Available maps:") + for idx, filename in enumerate(files): + print(f"{idx + 1}. {filename}") + + while True: + try: + choice = int(input("Choose a map by number: ")) - 1 + if 0 <= choice < len(files): + chosen_map = files[choice] + break + else: + print("Invalid choice.") + except ValueError: + print("Enter a valid number.") + + log.info(f"Loading map: {chosen_map}") + return Map.load_from_file(os.path.join(MAPS_DIRECTORY, chosen_map)).map_to_dict() + + +def start_server(host='127.0.0.1', port=5555): + log.info("Starting server...") + + map_data = choose_map() + + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((host, port)) + server.listen() + server.settimeout(0.5) # Periodically check for shutdown + + log.info(f"Listening on {host}:{port}") + + next_player_id = 1 + threads = [] + + # Signal handler to cleanly shutdown + def signal_handler(sig, frame): + log.info("Shutdown signal received.") + shutdown_event.set() + + signal.signal(signal.SIGINT, signal_handler) + + try: + while not shutdown_event.is_set(): + try: + conn, addr = server.accept() + except socket.timeout: + continue + except OSError: + break # Socket closed + + log.info(f"Accepted connection from {addr}") + player_id = next_player_id + next_player_id += 1 + + t = threading.Thread(target=handle_client, args=(conn, addr, player_id, map_data)) + t.start() + threads.append(t) + + except Exception as e: + log.error(f"Error in server loop: {e}") + + finally: + log.info("Closing server socket...") + server.close() + + log.info("Waiting for client threads to exit...") + shutdown_event.set() # Inform all threads to shut down + + for t in threads: + t.join() + + log.info("Server shutdown complete.") + sys.exit(0) + + +if __name__ == "__main__": + start_server()