From db2832c81cac39aa51a1f868da02e0dc92837321 Mon Sep 17 00:00:00 2001 From: Max <44461157+MxPerrot@users.noreply.github.com> Date: Sun, 25 Feb 2024 19:58:17 +0100 Subject: [PATCH 01/45] Simplifing FixAng function Changing the conditional statement to a modular operation. --- ray.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ray.py b/ray.py index ce533ce..0782e19 100644 --- a/ray.py +++ b/ray.py @@ -14,13 +14,8 @@ # Accesory function def FixAng(angle): - a = angle - if(angle>359): - a -= 360 - elif(angle<0): - a += 360 - return a - + return angle%360 + # The map that player exists in world = [ 1,1,1,1,1,1,1,1, From 59a93ad2a58b3d62f2e7f30e9a77e32fdab61f30 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 18 Mar 2025 15:07:30 +0100 Subject: [PATCH 02/45] fix: looking downard had no light collisions --- ray.py | 480 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 240 insertions(+), 240 deletions(-) diff --git a/ray.py b/ray.py index 0782e19..5cb0bcb 100644 --- a/ray.py +++ b/ray.py @@ -1,240 +1,240 @@ -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): - return angle%360 - -# 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() - +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): + return angle%360 + +# 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(sin(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() + From 44934314f2b2de1302b786e42e40a6b81bff8f5a Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 18 Mar 2025 21:08:09 +0100 Subject: [PATCH 03/45] backup: move old ray file for backup --- ray.py => old/ray.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) rename ray.py => old/ray.py (92%) diff --git a/ray.py b/old/ray.py similarity index 92% rename from ray.py rename to old/ray.py index 5cb0bcb..2b8ac37 100644 --- a/ray.py +++ b/old/ray.py @@ -12,10 +12,17 @@ ''' -# Accesory function -def FixAng(angle): - return angle%360 - +KEYS = { + "FORWARD":'z', + "LEFT":'q', + "BACKWARD":'s', + "RIGHT":'d', + "MISCELLANOUS":'a' +} + +FOV = 60 # 60 is best for now + + # The map that player exists in world = [ 1,1,1,1,1,1,1,1, @@ -33,6 +40,13 @@ def FixAng(angle): mapX = 8 mapY = 8 +# Player position variables +px, py, pa, pdx, pdy = 0,0,0,0,0 + +# Accesory function +def FixAng(angle): + return angle%360 + # Draws 2D map def drawMap2d(): @@ -50,9 +64,6 @@ def drawMap2d(): 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) @@ -71,23 +82,23 @@ def drawPlayer2d(px, py, pa, pdx, pdy): # Handles keyboard input callbacks def buttons(key, x, y): global px, py, pa, pdx, pdy - if(ord(key) == ord('w')): + if(ord(key) == ord(KEYS["FORWARD"])): px += pdx*5 py += pdy*5 - elif(ord(key) == ord('a')): + elif(ord(key) == ord(KEYS["LEFT"])): pa += 5 pa = FixAng(pa) pdx=cos(radians(pa)) pdy=-sin(radians(pa)) - elif(ord(key) == ord('d')): + elif(ord(key) == ord(KEYS["RIGHT"])): pa -= 5 pa = FixAng(pa) pdx=cos(radians(pa)) pdy=-sin(radians(pa)) - elif(ord(key) == ord('s')): + elif(ord(key) == ord(KEYS["BACKWARD"])): px -= pdx*5 py -= pdy*5 - elif(ord(key) == ord('q')): + elif(ord(key) == ord(KEYS["MISCELLANOUS"])): px = x py = y glutPostRedisplay() @@ -115,7 +126,7 @@ def drawRays2d(): #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 + for r in range(1, FOV): # We are drawing total 60 rays, for a 60 degree field of view # Checking vertical wall intercept dof, side, disV = 0, 0, 10000 @@ -230,7 +241,7 @@ def display(): glutInit() glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) glutInitWindowSize(1024, 510) -glutCreateWindow("pyopengl raycater") +glutCreateWindow(b"pyopengl raycater") init() glutDisplayFunc(display) glutIdleFunc(display) From 4e4eb58f20d029333c4ffdfc9717341cc949fdcd Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 18 Mar 2025 21:08:56 +0100 Subject: [PATCH 04/45] chore: update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file From 7aaaa833bf80f0c487b8f687740057f23c668805 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 18 Mar 2025 21:09:22 +0100 Subject: [PATCH 05/45] feat: use OOP to support multi player --- main.py | 83 +++++++++++++++++++++++++++++++++++++++++++ raycaster/Map.py | 28 +++++++++++++++ raycaster/Player.py | 60 +++++++++++++++++++++++++++++++ raycaster/Renderer.py | 58 ++++++++++++++++++++++++++++++ raycaster/__init__.py | 3 ++ 5 files changed, 232 insertions(+) create mode 100644 main.py create mode 100644 raycaster/Map.py create mode 100644 raycaster/Player.py create mode 100644 raycaster/Renderer.py create mode 100644 raycaster/__init__.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..3891c0e --- /dev/null +++ b/main.py @@ -0,0 +1,83 @@ +from raycaster import Map, Player, Renderer +from OpenGL.GL import * +from OpenGL.GLU import * +from OpenGL.GLUT import * +import time + +last_time = time.time() + +def keyboard_down(key, x, y): + try: + key_char = key.decode() + for player in players: + if key_char in player.key_bindings.values(): + player.keys_pressed.add(key_char) + except UnicodeDecodeError: + print(f"Ignored non-decodable key: {key}") + + +def keyboard_up(key, x, y): + try: + key_char = key.decode() + for player in players: + if key_char in player.keys_pressed: + player.keys_pressed.remove(key_char) + except UnicodeDecodeError: + print(f"Ignored non-decodable key: {key}") + + + +def display(): + 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) + + for player in players: + player.move(delta_time) # Pass delta_time + + renderer.draw_scene() + glutSwapBuffers() + + +# ====== Map and Players setup ====== +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_obj = Map(world, 8, 8, 64) + +player1 = Player(150, 400, 90, {'FORWARD': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'}, map_obj) +player2 = Player(300, 400, 90, {'FORWARD': 'i', 'BACKWARD': 'k', 'LEFT': 'j', 'RIGHT': 'l'}, map_obj) + +players = [player1, player2] +renderer = Renderer(map_obj, players) + + +# ====== GLUT Initialization ====== + +glutInit() +glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) +glutInitWindowSize(1024, 512) +glutCreateWindow(b"Raycasting Engine") + +# Set up 2D orthographic projection +glClearColor(0.3, 0.3, 0.3, 0) +gluOrtho2D(0, 1024, 512, 0) + +# Register Callbacks AFTER glutInit() +glutKeyboardFunc(keyboard_down) +glutKeyboardUpFunc(keyboard_up) +glutDisplayFunc(display) +glutIdleFunc(display) + +glutMainLoop() diff --git a/raycaster/Map.py b/raycaster/Map.py new file mode 100644 index 0000000..803c1a5 --- /dev/null +++ b/raycaster/Map.py @@ -0,0 +1,28 @@ +from OpenGL.GL import * + +class Map: + def __init__(self, grid, mapX, mapY, mapS): + self.grid = grid + self.mapX = mapX + self.mapY = mapY + self.mapS = mapS + + def is_wall(self, mx, my): + if 0 <= mx < self.mapX and 0 <= my < self.mapY: + return self.grid[my * self.mapX + mx] == 1 + return True # Out of bounds = wall + + def draw(self): + for y in range(self.mapY): + for x in range(self.mapX): + if self.grid[y * self.mapX + x] == 1: + glColor3f(1, 1, 1) + else: + glColor3f(0, 0, 0) + xo, yo = x * self.mapS, y * self.mapS + glBegin(GL_QUADS) + glVertex2i(xo + 1, yo + 1) + glVertex2i(xo + 1, yo + self.mapS - 1) + glVertex2i(xo + self.mapS - 1, yo + self.mapS - 1) + glVertex2i(xo + self.mapS - 1, yo + 1) + glEnd() diff --git a/raycaster/Player.py b/raycaster/Player.py new file mode 100644 index 0000000..90d1699 --- /dev/null +++ b/raycaster/Player.py @@ -0,0 +1,60 @@ +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 + + + def draw(self): + glColor3f(1, 1, 0) + glPointSize(8) + glLineWidth(3) + glBegin(GL_POINTS) + glVertex2i(int(self.px), int(self.py)) + glEnd() + glBegin(GL_LINES) + glVertex2i(int(self.px), int(self.py)) + glVertex2i(int(self.px + 20 * self.pdx), int(self.py + 20 * self.pdy)) + glEnd() diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py new file mode 100644 index 0000000..1429ff8 --- /dev/null +++ b/raycaster/Renderer.py @@ -0,0 +1,58 @@ +from math import sin, cos, radians, hypot +from OpenGL.GL import * + +class Renderer: + def __init__(self, map_obj, players): + self.map = map_obj + self.players = players + self.FOV = 60 + self.num_rays = 60 + + def draw_scene(self): + self.map.draw() + for player in self.players: + player.draw() + self.cast_rays(player) + + def cast_rays(self, player): + 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 # Track if wall is hit + + while dof < 20: # Max depth (higher allows longer rays) + mx = int(rx) // self.map.mapS + my = int(ry) // self.map.mapS + if mx < 0 or mx >= self.map.mapX or my < 0 or my >= self.map.mapY: + break # Ray exited map bounds, stop tracing + if self.map.grid[my * self.map.mapX + mx] == 1: + dis = hypot(rx - player.px, ry - player.py) + hit_wall = True + break + rx += cos(ray_angle) * 5 + ry -= sin(ray_angle) * 5 + dof += 0.1 + + if not hit_wall: + ra -= (self.FOV / self.num_rays) + continue # Skip drawing wall slice + + # Wall hit, draw projection: + ca = radians(player.pa - ra) + dis *= cos(ca) # Fix fisheye + line_height = (self.map.mapS * 320) / dis + if line_height > 320: + line_height = 320 + line_offset = 160 - line_height / 2 + + glColor3f(1.0, 0.0, 0.0) + glLineWidth(8) + glBegin(GL_LINES) + glVertex2i(r * 8 + 530, int(line_offset)) + glVertex2i(r * 8 + 530, int(line_offset + line_height)) + glEnd() + + ra -= (self.FOV / self.num_rays) 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 From 0d9c82c0c116cb3c43d3073ad8b57f23605bb20c Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 18 Mar 2025 21:35:41 +0100 Subject: [PATCH 06/45] feat: add local multiplayer (separate windows) --- main.py | 116 ++++++++++++++++++++++-------------------- raycaster/Renderer.py | 8 +-- 2 files changed, 64 insertions(+), 60 deletions(-) diff --git a/main.py b/main.py index 3891c0e..f732f5c 100644 --- a/main.py +++ b/main.py @@ -2,47 +2,62 @@ from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * +import multiprocessing import time -last_time = time.time() +# The per-player GLUT window runner +def run_player(player_id, world, key_bindings): + # Create map & player locally + map_obj = Map(world, 8, 8, 64) + player = Player(150 if player_id == 1 else 300, 400, 90, key_bindings, map_obj) + renderer = Renderer(map_obj, [player]) # Only render this player! -def keyboard_down(key, x, y): - try: - key_char = key.decode() - for player in players: + last_time = time.time() + + # Input handlers specific to THIS process + def keyboard_down(key, x, y): + try: + key_char = key.decode() if key_char in player.key_bindings.values(): player.keys_pressed.add(key_char) - except UnicodeDecodeError: - print(f"Ignored non-decodable key: {key}") - + except UnicodeDecodeError: + pass -def keyboard_up(key, x, y): - try: - key_char = key.decode() - for player in players: + def keyboard_up(key, x, y): + try: + key_char = key.decode() if key_char in player.keys_pressed: player.keys_pressed.remove(key_char) - except UnicodeDecodeError: - print(f"Ignored non-decodable key: {key}") - - - -def display(): - 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) - - for player in players: - player.move(delta_time) # Pass delta_time - - renderer.draw_scene() - glutSwapBuffers() - - -# ====== Map and Players setup ====== + except UnicodeDecodeError: + pass + + def display(): + nonlocal 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) + player.move(delta_time) + renderer.draw_scene(player) + glutSwapBuffers() + + # GLUT setup + glutInit() + glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) + glutInitWindowSize(1024, 512) + glutCreateWindow(f"Player {player_id}".encode()) + glClearColor(0.3, 0.3, 0.3, 0) + gluOrtho2D(0, 1024, 512, 0) + + glutKeyboardFunc(keyboard_down) + glutKeyboardUpFunc(keyboard_up) + glutDisplayFunc(display) + glutIdleFunc(display) + + glutMainLoop() + +# ==== Shared World Data ==== world = [ 1,1,1,1,1,1,1,1, 1,1,0,1,0,0,0,1, @@ -54,30 +69,19 @@ def display(): 1,0,0,0,0,0,0,1 ] -map_obj = Map(world, 8, 8, 64) - -player1 = Player(150, 400, 90, {'FORWARD': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'}, map_obj) -player2 = Player(300, 400, 90, {'FORWARD': 'i', 'BACKWARD': 'k', 'LEFT': 'j', 'RIGHT': 'l'}, map_obj) - -players = [player1, player2] -renderer = Renderer(map_obj, players) - - -# ====== GLUT Initialization ====== +if __name__ == "__main__": + multiprocessing.set_start_method('spawn') # Safer on Windows -glutInit() -glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) -glutInitWindowSize(1024, 512) -glutCreateWindow(b"Raycasting Engine") + # Define key bindings per player + player1_keys = {'FORWARD': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'} + player2_keys = {'FORWARD': 'i', 'BACKWARD': 'k', 'LEFT': 'j', 'RIGHT': 'l'} -# Set up 2D orthographic projection -glClearColor(0.3, 0.3, 0.3, 0) -gluOrtho2D(0, 1024, 512, 0) + # Create processes + p1 = multiprocessing.Process(target=run_player, args=(1, world, player1_keys)) + p2 = multiprocessing.Process(target=run_player, args=(2, world, player2_keys)) -# Register Callbacks AFTER glutInit() -glutKeyboardFunc(keyboard_down) -glutKeyboardUpFunc(keyboard_up) -glutDisplayFunc(display) -glutIdleFunc(display) + p1.start() + p2.start() -glutMainLoop() + p1.join() + p2.join() diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 1429ff8..542bf3e 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -8,11 +8,11 @@ def __init__(self, map_obj, players): self.FOV = 60 self.num_rays = 60 - def draw_scene(self): + def draw_scene(self, player): self.map.draw() - for player in self.players: - player.draw() - self.cast_rays(player) + player.draw() + self.cast_rays(player) # Raycasting specific to this player + def cast_rays(self, player): ra = player.pa + (self.FOV / 2) From 761bccc76ee98b3e207fe7dd9d980d1fe79e05a0 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 18 Mar 2025 21:49:22 +0100 Subject: [PATCH 07/45] feat: add server/client communication --- client_network.py | 34 +++++++++++ main.py | 149 ++++++++++++++++++++++++++-------------------- server.py | 0 server_network.py | 58 ++++++++++++++++++ 4 files changed, 175 insertions(+), 66 deletions(-) create mode 100644 client_network.py create mode 100644 server.py create mode 100644 server_network.py diff --git a/client_network.py b/client_network.py new file mode 100644 index 0000000..d3f4ff9 --- /dev/null +++ b/client_network.py @@ -0,0 +1,34 @@ +import socket +import threading +import json + +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.running = True + # Start listening thread + thread = threading.Thread(target=self.listen) + thread.start() + + def listen(self): + try: + while self.running: + data = self.server.recv(4096).decode() + if data: + self.players_state = json.loads(data) + except: + print("[CLIENT] Server disconnected.") + self.running = False + + def send_player_update(self, px, py, pa): + try: + message = {"px": px, "py": py, "pa": pa} + self.server.sendall(json.dumps(message).encode()) + except: + pass + + def close(self): + self.running = False + self.server.close() diff --git a/main.py b/main.py index f732f5c..513a209 100644 --- a/main.py +++ b/main.py @@ -2,62 +2,15 @@ from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * -import multiprocessing +from client_network import ClientNetwork import time +import threading + +# === NETWORK INITIALIZATION === +network = ClientNetwork() # Connects to server at localhost:5555 by default + +# === GAME INITIALIZATION === -# The per-player GLUT window runner -def run_player(player_id, world, key_bindings): - # Create map & player locally - map_obj = Map(world, 8, 8, 64) - player = Player(150 if player_id == 1 else 300, 400, 90, key_bindings, map_obj) - renderer = Renderer(map_obj, [player]) # Only render this player! - - last_time = time.time() - - # Input handlers specific to THIS process - def keyboard_down(key, x, y): - try: - key_char = key.decode() - 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() - if key_char in player.keys_pressed: - player.keys_pressed.remove(key_char) - except UnicodeDecodeError: - pass - - def display(): - nonlocal 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) - player.move(delta_time) - renderer.draw_scene(player) - glutSwapBuffers() - - # GLUT setup - glutInit() - glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) - glutInitWindowSize(1024, 512) - glutCreateWindow(f"Player {player_id}".encode()) - glClearColor(0.3, 0.3, 0.3, 0) - gluOrtho2D(0, 1024, 512, 0) - - glutKeyboardFunc(keyboard_down) - glutKeyboardUpFunc(keyboard_up) - glutDisplayFunc(display) - glutIdleFunc(display) - - glutMainLoop() - -# ==== Shared World Data ==== world = [ 1,1,1,1,1,1,1,1, 1,1,0,1,0,0,0,1, @@ -69,19 +22,83 @@ def display(): 1,0,0,0,0,0,0,1 ] -if __name__ == "__main__": - multiprocessing.set_start_method('spawn') # Safer on Windows +map_obj = Map(world, 8, 8, 64) + +# You can adjust keybindings per client instance +key_bindings = {'FORWARD': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'} +player = Player(150, 400, 90, key_bindings, map_obj) + +renderer = Renderer(map_obj, [player]) # Local player only for now + +last_time = time.time() + +# === INPUT HANDLERS === +def keyboard_down(key, x, y): + try: + key_char = key.decode() + 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() + if key_char in player.keys_pressed: + player.keys_pressed.remove(key_char) + except UnicodeDecodeError: + pass + +# === DISPLAY FUNCTION === +def display(): + 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) + glColor3f(0.0, 1.0, 0.0) # Different color for remote + for remote_id, remote_data in network.players_state.items(): + # Skip rendering ourself (if needed) + if abs(remote_data['px'] - player.px) < 1e-2 and abs(remote_data['py'] - player.py) < 1e-2: + continue + glPointSize(6) + glBegin(GL_POINTS) + glVertex2i(int(remote_data['px']), int(remote_data['py'])) + glEnd() + + glutSwapBuffers() + +# === GLUT INITIALIZATION === +glutInit() +glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) +glutInitWindowSize(1024, 512) +glutCreateWindow(b"Networked Raycaster Client") + +glClearColor(0.3, 0.3, 0.3, 0) +gluOrtho2D(0, 1024, 512, 0) - # Define key bindings per player - player1_keys = {'FORWARD': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'} - player2_keys = {'FORWARD': 'i', 'BACKWARD': 'k', 'LEFT': 'j', 'RIGHT': 'l'} +glutKeyboardFunc(keyboard_down) +glutKeyboardUpFunc(keyboard_up) +glutDisplayFunc(display) +glutIdleFunc(display) - # Create processes - p1 = multiprocessing.Process(target=run_player, args=(1, world, player1_keys)) - p2 = multiprocessing.Process(target=run_player, args=(2, world, player2_keys)) +# === CLEANUP === +def on_close(): + network.close() - p1.start() - p2.start() +import atexit +atexit.register(on_close) - p1.join() - p2.join() +glutMainLoop() diff --git a/server.py b/server.py new file mode 100644 index 0000000..e69de29 diff --git a/server_network.py b/server_network.py new file mode 100644 index 0000000..e1a819b --- /dev/null +++ b/server_network.py @@ -0,0 +1,58 @@ +import socket +import threading +import json + +clients = {} # key: conn, value: player_id +players_state = {} # key: player_id, value: {px, py, pa} + +def handle_client(conn, addr, player_id): + print(f"[SERVER] New connection from {addr}") + clients[conn] = player_id + try: + while True: + data = conn.recv(1024).decode() + if not data: + break + # Update player state + message = json.loads(data) + players_state[player_id] = message + + # Broadcast to all clients + broadcast() + except Exception as e: + print(f"[SERVER] Error: {e}") + finally: + print(f"[SERVER] Connection lost: {addr}") + conn.close() + clients.pop(conn, None) + players_state.pop(player_id, None) + +def broadcast(): + for conn in clients: + try: + # Send full game state + state_json = json.dumps(players_state) + conn.sendall(state_json.encode()) + except: + pass # Ignore send errors for now + +def start_server(host='127.0.0.1', port=5555): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.bind((host, port)) + server.listen() + print(f"[SERVER] Listening on {host}:{port}") + + next_player_id = 1 + + while True: + conn, addr = server.accept() + player_id = next_player_id + next_player_id += 1 + + # Init player + players_state[player_id] = {"px": 150, "py": 400, "pa": 90} + thread = threading.Thread(target=handle_client, args=(conn, addr, player_id)) + thread.start() + +if __name__ == "__main__": + start_server() From f23d77f0d0dc9037828f37e73749106aaa2e85b3 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 18 Mar 2025 23:22:45 +0100 Subject: [PATCH 08/45] fix: players desynchronised when over 2 connected --- client_network.py | 58 ++++++++++++++++++++++++++------ main.py | 6 ++-- server_network.py | 84 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 116 insertions(+), 32 deletions(-) diff --git a/client_network.py b/client_network.py index d3f4ff9..0e72e27 100644 --- a/client_network.py +++ b/client_network.py @@ -1,33 +1,71 @@ import socket import threading import json +import struct 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 - # Start listening thread - thread = threading.Thread(target=self.listen) + + # 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: - data = self.server.recv(4096).decode() - if data: - self.players_state = json.loads(data) - except: - print("[CLIENT] Server disconnected.") + # Step 1: Read length + raw_len = self.recv_exact(self.server, 4) + if not raw_len: + break + msg_len = struct.unpack('!I', raw_len)[0] + + # Step 2: Read full message + data = self.recv_exact(self.server, msg_len) + if not data: + break + + message = json.loads(data.decode()) + + # Handle message + if 'init_id' in message: + self.my_id = message['init_id'] + print(f"[CLIENT] Assigned Player ID: {self.my_id}") + + # Send ACK + ack = json.dumps({"ack": True}).encode() + length = struct.pack('!I', len(ack)) + self.server.sendall(length + ack) + print("[CLIENT] Sent ACK to server") + + else: + self.players_state = message + print(f"[CLIENT] Updated players_state: {self.players_state}") + except Exception as e: + print(f"[CLIENT] Listen error: {e}") self.running = False def send_player_update(self, px, py, pa): try: message = {"px": px, "py": py, "pa": pa} - self.server.sendall(json.dumps(message).encode()) - except: - pass + message_bytes = json.dumps(message).encode() + length = struct.pack('!I', len(message_bytes)) + self.server.sendall(length + message_bytes) + except Exception as e: + print(f"[CLIENT] Send error: {e}") def close(self): self.running = False diff --git a/main.py b/main.py index 513a209..551299f 100644 --- a/main.py +++ b/main.py @@ -70,8 +70,8 @@ def display(): # Draw remote players (optional, if desired) glColor3f(0.0, 1.0, 0.0) # Different color for remote for remote_id, remote_data in network.players_state.items(): - # Skip rendering ourself (if needed) - if abs(remote_data['px'] - player.px) < 1e-2 and abs(remote_data['py'] - player.py) < 1e-2: + # Skip rendering ourself + if remote_id == str(network.my_id): # IDs are string keys after JSON continue glPointSize(6) glBegin(GL_POINTS) @@ -79,6 +79,8 @@ def display(): glEnd() glutSwapBuffers() + print(f"[CLIENT] Rendering players_state: {network.players_state}") + # === GLUT INITIALIZATION === glutInit() diff --git a/server_network.py b/server_network.py index e1a819b..ae82d10 100644 --- a/server_network.py +++ b/server_network.py @@ -1,40 +1,82 @@ import socket import threading import json +import struct -clients = {} # key: conn, value: player_id -players_state = {} # key: player_id, value: {px, py, pa} +clients = {} # conn: player_id +players_state = {} # player_id: {px, py, pa} + +# === 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(): + print(f"[SERVER] Broadcasting: {players_state}") + for conn in list(clients.keys()): + try: + send_message(conn, players_state) + except Exception as e: + print(f"[SERVER] Broadcast error: {e}") + conn.close() + clients.pop(conn, None) def handle_client(conn, addr, player_id): print(f"[SERVER] New connection from {addr}") clients[conn] = player_id + + # Step 1: Send init_id + send_message(conn, {"init_id": player_id}) + print(f"[SERVER] Sent init_id to {addr}") + + # Step 2: Wait for ACK before broadcasting + raw_len = recv_exact(conn, 4) + if not raw_len: + print(f"[SERVER] No ACK received from {addr}") + return + msg_len = struct.unpack('!I', raw_len)[0] + data = recv_exact(conn, msg_len) + ack_msg = json.loads(data.decode()) + if 'ack' not in ack_msg: + print(f"[SERVER] Invalid ACK from {addr}") + return + print(f"[SERVER] Received ACK from {addr}") + + # Now safe to broadcast + broadcast() + try: while True: - data = conn.recv(1024).decode() - if not data: + raw_len = recv_exact(conn, 4) + if not raw_len: break - # Update player state - message = json.loads(data) + 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 to all clients broadcast() except Exception as e: print(f"[SERVER] Error: {e}") finally: print(f"[SERVER] Connection lost: {addr}") conn.close() - clients.pop(conn, None) - players_state.pop(player_id, None) - -def broadcast(): - for conn in clients: - try: - # Send full game state - state_json = json.dumps(players_state) - conn.sendall(state_json.encode()) - except: - pass # Ignore send errors for now + if conn in clients: + clients.pop(conn) + if player_id in players_state: + players_state.pop(player_id) + broadcast() def start_server(host='127.0.0.1', port=5555): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -49,9 +91,11 @@ def start_server(host='127.0.0.1', port=5555): player_id = next_player_id next_player_id += 1 - # Init player + # Initialize player's state players_state[player_id] = {"px": 150, "py": 400, "pa": 90} - thread = threading.Thread(target=handle_client, args=(conn, addr, player_id)) + + # Start handler + thread = threading.Thread(target=handle_client, args=(conn, addr, player_id), daemon=True) thread.start() if __name__ == "__main__": From 5da4a7c832de9b0853e0939ebdfe1c7946208eb9 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 02:02:49 +0100 Subject: [PATCH 09/45] feat: make map server-side --- client_network.py | 6 ++++++ main.py | 17 +++++------------ server_network.py | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/client_network.py b/client_network.py index 0e72e27..874a43f 100644 --- a/client_network.py +++ b/client_network.py @@ -10,6 +10,8 @@ def __init__(self, host='127.0.0.1', port=5555): self.players_state = {} self.my_id = None self.running = True + self.map_data = None + # Start listening thread = threading.Thread(target=self.listen, daemon=True) @@ -51,6 +53,10 @@ def listen(self): self.server.sendall(length + ack) print("[CLIENT] Sent ACK to server") + elif 'map_data' in message: + self.map_data = message['map_data'] + print(f"[CLIENT] Received map data: {self.map_data}") + else: self.players_state = message print(f"[CLIENT] Updated players_state: {self.players_state}") diff --git a/main.py b/main.py index 551299f..69dfdf3 100644 --- a/main.py +++ b/main.py @@ -10,19 +10,12 @@ network = ClientNetwork() # Connects to server at localhost:5555 by default # === GAME INITIALIZATION === +# Wait for map data +while network.map_data is None: + print("[CLIENT] Waiting for map...") -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_obj = Map(world, 8, 8, 64) +map_info = network.map_data +map_obj = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) # You can adjust keybindings per client instance key_bindings = {'FORWARD': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'} diff --git a/server_network.py b/server_network.py index ae82d10..95de42a 100644 --- a/server_network.py +++ b/server_network.py @@ -6,6 +6,25 @@ clients = {} # conn: player_id players_state = {} # player_id: {px, py, pa} +# Server Map Definition +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_data = { + "grid": world, + "mapX": 8, + "mapY": 8, + "mapS": 64 +} + # === Helper functions === def send_message(conn, message_dict): @@ -40,6 +59,10 @@ def handle_client(conn, addr, player_id): send_message(conn, {"init_id": player_id}) print(f"[SERVER] Sent init_id to {addr}") + # Send map data + send_message(conn, {"map_data": map_data}) + print(f"[SERVER] Sent map_data to {addr}") + # Step 2: Wait for ACK before broadcasting raw_len = recv_exact(conn, 4) if not raw_len: From 159417e04064d73742e9c2c6d8b7d06bde56579a Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 10:41:38 +0100 Subject: [PATCH 10/45] docs: potential bug comment --- raycaster/Renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 542bf3e..19adbc9 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -43,7 +43,7 @@ def cast_rays(self, player): # Wall hit, draw projection: ca = radians(player.pa - ra) dis *= cos(ca) # Fix fisheye - line_height = (self.map.mapS * 320) / dis + line_height = (self.map.mapS * 320) / dis # [ ] FIXME Potential division by 0 if line_height > 320: line_height = 320 line_offset = 160 - line_height / 2 From 80e780554495ee3bb34669cfd5f05d28c19e58c5 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 11:03:33 +0100 Subject: [PATCH 11/45] chore: delete old/unused files --- old/ray.py | 251 ----------------------------------------------------- server.py | 0 2 files changed, 251 deletions(-) delete mode 100644 old/ray.py delete mode 100644 server.py diff --git a/old/ray.py b/old/ray.py deleted file mode 100644 index 2b8ac37..0000000 --- a/old/ray.py +++ /dev/null @@ -1,251 +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 - - -''' - -KEYS = { - "FORWARD":'z', - "LEFT":'q', - "BACKWARD":'s', - "RIGHT":'d', - "MISCELLANOUS":'a' -} - -FOV = 60 # 60 is best for now - - -# 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 - -# Player position variables -px, py, pa, pdx, pdy = 0,0,0,0,0 - -# Accesory function -def FixAng(angle): - return angle%360 - -# 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() - -# 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(KEYS["FORWARD"])): - px += pdx*5 - py += pdy*5 - elif(ord(key) == ord(KEYS["LEFT"])): - pa += 5 - pa = FixAng(pa) - pdx=cos(radians(pa)) - pdy=-sin(radians(pa)) - elif(ord(key) == ord(KEYS["RIGHT"])): - pa -= 5 - pa = FixAng(pa) - pdx=cos(radians(pa)) - pdy=-sin(radians(pa)) - elif(ord(key) == ord(KEYS["BACKWARD"])): - px -= pdx*5 - py -= pdy*5 - elif(ord(key) == ord(KEYS["MISCELLANOUS"])): - 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, FOV): # 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(sin(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(b"pyopengl raycater") -init() -glutDisplayFunc(display) -glutIdleFunc(display) -glutKeyboardFunc(buttons) - -glutMainLoop() - diff --git a/server.py b/server.py deleted file mode 100644 index e69de29..0000000 From afa4a5a3ef0f85ae2f937ca09c50a5835f6a6de1 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 11:40:18 +0100 Subject: [PATCH 12/45] feat: made map savable/loadable --- raycaster/Map.py | 126 ++++++++++++++++++++++++++++++++++++++++++++-- server_network.py | 29 ++++------- 2 files changed, 134 insertions(+), 21 deletions(-) diff --git a/raycaster/Map.py b/raycaster/Map.py index 803c1a5..6faba70 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -1,11 +1,12 @@ from OpenGL.GL import * +import json class Map: def __init__(self, grid, mapX, mapY, mapS): self.grid = grid - self.mapX = mapX - self.mapY = mapY - self.mapS = mapS + self.mapX = mapX # Map width + self.mapY = mapY # Map height + self.mapS = mapS # Tile size in pixels. Should be 32 to 128. Ideally 64 def is_wall(self, mx, my): if 0 <= mx < self.mapX and 0 <= my < self.mapY: @@ -26,3 +27,122 @@ def draw(self): glVertex2i(xo + self.mapS - 1, yo + self.mapS - 1) glVertex2i(xo + self.mapS - 1, yo + 1) glEnd() + + # 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 + } + with open(filename, 'w') as f: + json.dump(map_data, f) + print(f"[MAP] Saved map to {filename}") + + @classmethod + def load_from_file(cls, filename): + with open(filename, 'r') as f: + map_data = json.load(f) + print(f"[MAP] Loaded map from {filename}") + return cls(map_data["grid"], map_data["mapX"], map_data["mapY"], map_data["mapS"]) + + def map_to_dict(self): + return { + "grid": self.grid, + "mapX": self.mapX, + "mapY": self.mapY, + "mapS": self.mapS + } + + +if __name__ == "__main__": + """ + Test of the class and its methods + """ + + 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_info = { + "grid": world, + "mapX": 8, + "mapY": 8, + "mapS": 64 + } + + map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) + + map.save_to_file('maps/aryantech123.json') + + # MAP 2 + + world = [ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ] + + map_info = { + "grid": world, + "mapX": 16, + "mapY": 16, + "mapS": 64 + } + + map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) + + map.save_to_file('maps/empty_l.json') + + # MAP 3 + + world = [ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,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 + ] + + map_info = { + "grid": world, + "mapX": 16, + "mapY": 16, + "mapS": 64 + } + + map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) + + map.save_to_file('maps/house_l.json') \ No newline at end of file diff --git a/server_network.py b/server_network.py index 95de42a..c811265 100644 --- a/server_network.py +++ b/server_network.py @@ -2,28 +2,14 @@ import threading import json import struct +from raycaster import Map clients = {} # conn: player_id players_state = {} # player_id: {px, py, pa} # Server Map Definition -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_data = { - "grid": world, - "mapX": 8, - "mapY": 8, - "mapS": 64 -} + +map_data = Map.load_from_file('maps/house_l.json').map_to_dict() # === Helper functions === @@ -70,7 +56,14 @@ def handle_client(conn, addr, player_id): return msg_len = struct.unpack('!I', raw_len)[0] data = recv_exact(conn, msg_len) - ack_msg = json.loads(data.decode()) + #[ ] FIXME: make sure to only broadcast to those with ack recieved / close connexion when invalid ack. + # Otherwise you can join from browser, close the page, connexion is never closed and it keeps broadcasting to you. + # Check with chatgpt if this might cause issues with normal clients + try: + ack_msg = json.loads(data.decode()) + except: + ack_msg = {} + print(f"[SERVER] No ACK from {addr}") if 'ack' not in ack_msg: print(f"[SERVER] Invalid ACK from {addr}") return From 3808fc4552cf0b7cc3d2487212780827ae83f1e8 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 11:41:11 +0100 Subject: [PATCH 13/45] add some maps --- maps/aryantech123.json | 1 + maps/empty_l.json | 1 + maps/house_l.json | 1 + maps/map.json | 1 + 4 files changed, 4 insertions(+) create mode 100644 maps/aryantech123.json create mode 100644 maps/empty_l.json create mode 100644 maps/house_l.json create mode 100644 maps/map.json diff --git a/maps/aryantech123.json b/maps/aryantech123.json new file mode 100644 index 0000000..54635a9 --- /dev/null +++ b/maps/aryantech123.json @@ -0,0 +1 @@ +{"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} \ No newline at end of file diff --git a/maps/empty_l.json b/maps/empty_l.json new file mode 100644 index 0000000..05f2d14 --- /dev/null +++ b/maps/empty_l.json @@ -0,0 +1 @@ +{"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} \ No newline at end of file diff --git a/maps/house_l.json b/maps/house_l.json new file mode 100644 index 0000000..1a91973 --- /dev/null +++ b/maps/house_l.json @@ -0,0 +1 @@ +{"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} \ No newline at end of file diff --git a/maps/map.json b/maps/map.json new file mode 100644 index 0000000..54635a9 --- /dev/null +++ b/maps/map.json @@ -0,0 +1 @@ +{"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} \ No newline at end of file From 60460845984a286aa1cb9cb25cc18e988ffd7e34 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 11:48:13 +0100 Subject: [PATCH 14/45] docs: add TODO file --- TODO | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..b74fac9 --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +[ ] Change lighting depending on distance and facing (n/s/e/w) +[ ] Add sky and ground (top/bottom rectangle) +[ ] Create a map manager/editor \ No newline at end of file From 62b0d1d304a37421739b86a4575b393e7c840abd Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 12:13:32 +0100 Subject: [PATCH 15/45] docs: update README.md & TODO --- README.md | 48 +++++++++++++++++++++++++++++++++++++++--------- TODO | 3 ++- image.png | Bin 36975 -> 0 bytes 3 files changed, 41 insertions(+), 10 deletions(-) delete mode 100644 image.png diff --git a/README.md b/README.md index 5e15191..f715989 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,51 @@ -# 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 + - Sky/Ground + - Directional light source (N/S/W/E) + - Lighting depends on distance ## 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 index b74fac9..75f4d64 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,4 @@ [ ] Change lighting depending on distance and facing (n/s/e/w) [ ] Add sky and ground (top/bottom rectangle) -[ ] Create a map manager/editor \ No newline at end of file +[ ] Create a map manager/editor +[ ] Detect keyboard layout for key bindings \ No newline at end of file diff --git a/image.png b/image.png deleted file mode 100644 index 65d0f02cd1d375210f46756a34f9536505736b90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36975 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 TJ^6qf`1gbTw)NR-@lpQ Date: Wed, 19 Mar 2025 14:49:06 +0100 Subject: [PATCH 16/45] feat: add sky/ground and colorMap to map --- README.md | 1 - TODO | 1 - main.py | 2 +- maps/aryantech123.json | 2 +- maps/empty_l.json | 2 +- maps/house_l.json | 2 +- raycaster/Map.py | 43 ++++++++++++++++++++++++++++++++---------- raycaster/Renderer.py | 25 +++++++++++++++++++++++- 8 files changed, 61 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f715989..74bb06f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ Currently I plan on making - Map generation (maybe mazelik/terrainlike) - Game mechanics (not yet determined) - Improve graphics - - Sky/Ground - Directional light source (N/S/W/E) - Lighting depends on distance diff --git a/TODO b/TODO index 75f4d64..84e7719 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,3 @@ [ ] Change lighting depending on distance and facing (n/s/e/w) -[ ] Add sky and ground (top/bottom rectangle) [ ] Create a map manager/editor [ ] Detect keyboard layout for key bindings \ No newline at end of file diff --git a/main.py b/main.py index 69dfdf3..f6e5f79 100644 --- a/main.py +++ b/main.py @@ -15,7 +15,7 @@ print("[CLIENT] Waiting for map...") map_info = network.map_data -map_obj = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) +map_obj = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info["colorMap"]) # You can adjust keybindings per client instance key_bindings = {'FORWARD': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'} diff --git a/maps/aryantech123.json b/maps/aryantech123.json index 54635a9..896341a 100644 --- a/maps/aryantech123.json +++ b/maps/aryantech123.json @@ -1 +1 @@ -{"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} \ No newline at end of file +{"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]}} \ No newline at end of file diff --git a/maps/empty_l.json b/maps/empty_l.json index 05f2d14..96ccabb 100644 --- a/maps/empty_l.json +++ b/maps/empty_l.json @@ -1 +1 @@ -{"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} \ No newline at end of file +{"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]}} \ No newline at end of file diff --git a/maps/house_l.json b/maps/house_l.json index 1a91973..8fbc674 100644 --- a/maps/house_l.json +++ b/maps/house_l.json @@ -1 +1 @@ -{"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} \ No newline at end of file +{"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], "wall": [100,100,100]}} \ No newline at end of file diff --git a/raycaster/Map.py b/raycaster/Map.py index 6faba70..59b1d12 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -2,11 +2,12 @@ import json class Map: - def __init__(self, grid, mapX, mapY, mapS): + def __init__(self, grid, mapX, mapY, mapS, colorMap): 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 def is_wall(self, mx, my): if 0 <= mx < self.mapX and 0 <= my < self.mapY: @@ -34,7 +35,8 @@ def save_to_file(self, filename): "grid": self.grid, "mapX": self.mapX, "mapY": self.mapY, - "mapS": self.mapS + "mapS": self.mapS, + "colorMap": self.colorMap } with open(filename, 'w') as f: json.dump(map_data, f) @@ -45,15 +47,27 @@ def load_from_file(cls, filename): with open(filename, 'r') as f: map_data = json.load(f) print(f"[MAP] Loaded map from {filename}") - return cls(map_data["grid"], map_data["mapX"], map_data["mapY"], map_data["mapS"]) + return cls(map_data["grid"], map_data["mapX"], map_data["mapY"], map_data["mapS"], map_data["colorMap"]) def map_to_dict(self): return { "grid": self.grid, "mapX": self.mapX, "mapY": self.mapY, - "mapS": self.mapS + "mapS": self.mapS, + "colorMap": self.colorMap } + + def get_color(self, texture): + try: + color = self.colorMap[texture] + except: + color = (1,0,1) # MAGENTA FOR ERROR + print(f"[MAP] TEXTURE {texture} DOES NOT EXIST") + return color + + + if __name__ == "__main__": @@ -72,14 +86,21 @@ def map_to_dict(self): 1,0,0,0,0,0,0,1 ] + colorMap = { + "ground":(74,194,44), + "sky":(235,255,254), + "wall":(100,100,100) + } + map_info = { "grid": world, "mapX": 8, "mapY": 8, - "mapS": 64 + "mapS": 64, + "colorMap":colorMap } - map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) + map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info['colorMap']) map.save_to_file('maps/aryantech123.json') @@ -108,10 +129,11 @@ def map_to_dict(self): "grid": world, "mapX": 16, "mapY": 16, - "mapS": 64 + "mapS": 64, + "colorMap":colorMap } - map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) + map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info['colorMap']) map.save_to_file('maps/empty_l.json') @@ -140,9 +162,10 @@ def map_to_dict(self): "grid": world, "mapX": 16, "mapY": 16, - "mapS": 64 + "mapS": 32, + "colorMap":colorMap } - map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS']) + map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info['colorMap']) map.save_to_file('maps/house_l.json') \ No newline at end of file diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 19adbc9..f7bb5fd 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -15,6 +15,29 @@ def draw_scene(self, player): def cast_rays(self, player): + + # Draws sky + sky_color = self.map.get_color("sky") + print(f"--- SKY COLOR {sky_color} ---") + glColor3ub(*sky_color) + glBegin(GL_QUADS) + glVertex(526, 0) + glVertex(1006, 0) + glVertex(1006,160) + glVertex(526,160) + glEnd() + + #Draws floor + ground_color = self.map.get_color("ground") + print(f"--- GROUND COLOR {ground_color} ---") + glColor3ub(*ground_color) + glBegin(GL_QUADS) + glVertex2i(526,160) + glVertex2i(1006,160) + glVertex2i(1006,320) + glVertex2i(526,320) + glEnd() + ra = player.pa + (self.FOV / 2) for r in range(self.num_rays): ray_angle = radians(ra) @@ -48,7 +71,7 @@ def cast_rays(self, player): line_height = 320 line_offset = 160 - line_height / 2 - glColor3f(1.0, 0.0, 0.0) + glColor3ub(*self.map.get_color("wall")) glLineWidth(8) glBegin(GL_LINES) glVertex2i(r * 8 + 530, int(line_offset)) From 2cd60065713ccb6855e33ef81ce0a6618277c166 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 14:49:35 +0100 Subject: [PATCH 17/45] chore: update LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) 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 From 11ae2680bf953ce4cc28c2a37ee2ef6138d77cdc Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 17:38:58 +0100 Subject: [PATCH 18/45] logs: comment useless prints --- client_network.py | 2 +- main.py | 2 +- raycaster/Renderer.py | 2 -- server_network.py | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client_network.py b/client_network.py index 874a43f..a5afda2 100644 --- a/client_network.py +++ b/client_network.py @@ -59,7 +59,7 @@ def listen(self): else: self.players_state = message - print(f"[CLIENT] Updated players_state: {self.players_state}") + # print(f"[CLIENT] Updated players_state: {self.players_state}") # DEBUG except Exception as e: print(f"[CLIENT] Listen error: {e}") self.running = False diff --git a/main.py b/main.py index f6e5f79..aae43f2 100644 --- a/main.py +++ b/main.py @@ -72,7 +72,7 @@ def display(): glEnd() glutSwapBuffers() - print(f"[CLIENT] Rendering players_state: {network.players_state}") + # print(f"[CLIENT] Rendering players_state: {network.players_state}") # DEBUG # === GLUT INITIALIZATION === diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index f7bb5fd..d11e7d8 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -18,7 +18,6 @@ def cast_rays(self, player): # Draws sky sky_color = self.map.get_color("sky") - print(f"--- SKY COLOR {sky_color} ---") glColor3ub(*sky_color) glBegin(GL_QUADS) glVertex(526, 0) @@ -29,7 +28,6 @@ def cast_rays(self, player): #Draws floor ground_color = self.map.get_color("ground") - print(f"--- GROUND COLOR {ground_color} ---") glColor3ub(*ground_color) glBegin(GL_QUADS) glVertex2i(526,160) diff --git a/server_network.py b/server_network.py index c811265..d6b3853 100644 --- a/server_network.py +++ b/server_network.py @@ -28,7 +28,7 @@ def recv_exact(sock, n): return data def broadcast(): - print(f"[SERVER] Broadcasting: {players_state}") + # print(f"[SERVER] Broadcasting: {players_state}") # DEBUG for conn in list(clients.keys()): try: send_message(conn, players_state) From c4e68a8af13bbcfb7c4ee405c3d21b6ffc11e9bd Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 17:39:36 +0100 Subject: [PATCH 19/45] feat: working face direction shading (gray only) --- raycaster/Renderer.py | 50 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index d11e7d8..8e795ee 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -44,18 +44,44 @@ def cast_rays(self, player): rx, ry = player.px, player.py hit_wall = False # Track if wall is hit - while dof < 20: # Max depth (higher allows longer rays) + while dof < 20: + # Store previous grid cell BEFORE moving + prev_mx = int(rx) // self.map.mapS + prev_my = int(ry) // self.map.mapS + + # Move ray + rx += cos(ray_angle) * 5 + ry -= sin(ray_angle) * 5 + dof += 0.1 + + # Current grid cell mx = int(rx) // self.map.mapS my = int(ry) // self.map.mapS + if mx < 0 or mx >= self.map.mapX or my < 0 or my >= self.map.mapY: - break # Ray exited map bounds, stop tracing + break + if self.map.grid[my * self.map.mapX + mx] == 1: dis = hypot(rx - player.px, ry - player.py) hit_wall = True + + # Compare previous and current grid cells + delta_mx = mx - prev_mx + delta_my = my - prev_my + + if abs(delta_mx) > abs(delta_my): + if delta_mx > 0: + wall_face = "West" + else: + wall_face = "East" + else: + if delta_my > 0: + wall_face = "North" + else: + wall_face = "South" + break - rx += cos(ray_angle) * 5 - ry -= sin(ray_angle) * 5 - dof += 0.1 + if not hit_wall: ra -= (self.FOV / self.num_rays) @@ -69,7 +95,19 @@ def cast_rays(self, player): line_height = 320 line_offset = 160 - line_height / 2 - glColor3ub(*self.map.get_color("wall")) + if wall_face == "North": + shaded_color = (200, 200, 200) + elif wall_face == "South": + shaded_color = (180, 180, 180) + elif wall_face == "East": + shaded_color = (160, 160, 160) + elif wall_face == "West": + shaded_color = (140, 140, 140) + else: + shaded_color = (255, 0, 255) + + glColor3ub(*shaded_color) + glLineWidth(8) glBegin(GL_LINES) glVertex2i(r * 8 + 530, int(line_offset)) From f1882e69017c28367e48d6c0bbdf9dcacd84af84 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 18:21:22 +0100 Subject: [PATCH 20/45] feat: graphics: shading depends on distance & face --- README.md | 3 +-- TODO | 5 +++-- maps/house_l.json | 2 +- raycaster/Renderer.py | 39 ++++++++++++++++++++++++++++----------- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 74bb06f..2910ec9 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ Currently I plan on making - Map generation (maybe mazelik/terrainlike) - Game mechanics (not yet determined) - Improve graphics - - Directional light source (N/S/W/E) - - Lighting depends on distance + - Use texture mapping ## Screenshots ![8kSpaceInvader's old version](https://github.com/ARYANTECH123/pythonRaycaster/blob/main/image.png) diff --git a/TODO b/TODO index 84e7719..04a02a1 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,4 @@ -[ ] Change lighting depending on distance and facing (n/s/e/w) [ ] Create a map manager/editor -[ ] Detect keyboard layout for key bindings \ No newline at end of file +[ ] Detect keyboard layout for key bindings +[ ] Add different kind of walls +[ ] Use texture mapping \ No newline at end of file diff --git a/maps/house_l.json b/maps/house_l.json index 8fbc674..c877725 100644 --- a/maps/house_l.json +++ b/maps/house_l.json @@ -1 +1 @@ -{"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], "wall": [100,100,100]}} \ No newline at end of file +{"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], "wall": [219,73,0]}} \ No newline at end of file diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 8e795ee..5e5aee5 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -15,6 +15,8 @@ def draw_scene(self, player): def cast_rays(self, player): + + max_distance = 1000 # For shading according to distance # Draws sky sky_color = self.map.get_color("sky") @@ -79,7 +81,7 @@ def cast_rays(self, player): wall_face = "North" else: wall_face = "South" - + break @@ -94,20 +96,35 @@ def cast_rays(self, player): if line_height > 320: line_height = 320 line_offset = 160 - line_height / 2 + base_color = self.map.get_color("wall") + + # Control how fast the damping applies: + distance_factor = max(0.4, 1 - dis / max_distance) # Keep at least 40% brightness + + # Shading + shade_factors = { + "North": 1.0, + "South": 0.6, + "East": 0.8, + "West": 0.9 + } + + # Wall face factor: + face_factor = shade_factors.get(wall_face, 1.0) + + # Final factor: + final_factor = face_factor * distance_factor - if wall_face == "North": - shaded_color = (200, 200, 200) - elif wall_face == "South": - shaded_color = (180, 180, 180) - elif wall_face == "East": - shaded_color = (160, 160, 160) - elif wall_face == "West": - shaded_color = (140, 140, 140) - else: - shaded_color = (255, 0, 255) + # Apply: + shaded_color = tuple( + max(0, min(255, int(c * final_factor))) + for c in base_color + ) + # Change color glColor3ub(*shaded_color) + # Draw vertical line according to ray glLineWidth(8) glBegin(GL_LINES) glVertex2i(r * 8 + 530, int(line_offset)) From 1058ad28cdb0c28a062118bcbfb33b288a9990d6 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 18:49:26 +0100 Subject: [PATCH 21/45] build: package multiplayer classes --- client_network.py => multiplayer/ClientNetwork.py | 0 multiplayer/__init__.py | 1 + 2 files changed, 1 insertion(+) rename client_network.py => multiplayer/ClientNetwork.py (100%) create mode 100644 multiplayer/__init__.py diff --git a/client_network.py b/multiplayer/ClientNetwork.py similarity index 100% rename from client_network.py rename to multiplayer/ClientNetwork.py 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 From f5a74520d682172da3061be889bc35de308beddf Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 18:50:07 +0100 Subject: [PATCH 22/45] build: fix multiplayer package import --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index aae43f2..69397a5 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * -from client_network import ClientNetwork +from multiplayer import ClientNetwork import time import threading From b57b46c8fee0f54d658eaa2d8edc150d8737e31e Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 18:50:33 +0100 Subject: [PATCH 23/45] feat: add menu for map choice before server start --- maps/aryantech123.json | 2 +- maps/empty_l.json | 2 +- maps/map.json | 2 +- server_network.py | 51 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/maps/aryantech123.json b/maps/aryantech123.json index 896341a..5bcf942 100644 --- a/maps/aryantech123.json +++ b/maps/aryantech123.json @@ -1 +1 @@ -{"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]}} \ No newline at end of file +{"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], "wall": [219,73,0]}} \ No newline at end of file diff --git a/maps/empty_l.json b/maps/empty_l.json index 96ccabb..68ff7ce 100644 --- a/maps/empty_l.json +++ b/maps/empty_l.json @@ -1 +1 @@ -{"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]}} \ No newline at end of file +{"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], "wall": [219,73,0]}} \ No newline at end of file diff --git a/maps/map.json b/maps/map.json index 54635a9..5bcf942 100644 --- a/maps/map.json +++ b/maps/map.json @@ -1 +1 @@ -{"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} \ No newline at end of file +{"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], "wall": [219,73,0]}} \ No newline at end of file diff --git a/server_network.py b/server_network.py index d6b3853..2c5f21c 100644 --- a/server_network.py +++ b/server_network.py @@ -2,15 +2,13 @@ import threading import json import struct +import sys +import os from raycaster import Map clients = {} # conn: player_id players_state = {} # player_id: {px, py, pa} -# Server Map Definition - -map_data = Map.load_from_file('maps/house_l.json').map_to_dict() - # === Helper functions === def send_message(conn, message_dict): @@ -37,7 +35,7 @@ def broadcast(): conn.close() clients.pop(conn, None) -def handle_client(conn, addr, player_id): +def handle_client(conn, addr, player_id, map_data): print(f"[SERVER] New connection from {addr}") clients[conn] = player_id @@ -94,7 +92,44 @@ def handle_client(conn, addr, player_id): players_state.pop(player_id) broadcast() + +def choose_map(): + MAPS_DIRECTORY = 'maps/' + + # List all files in the maps directory + 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 in the directory!") + return None + + # Display files + print("Available maps:") + for idx, filename in enumerate(files): + print(f"{idx + 1}. {filename}") + + # Ask user to choose + while True: + try: + choice = int(input("Choose a map by number: ")) - 1 + if 0 <= choice < len(files): + choosen_map = files[choice] + break + else: + print("Invalid choice. Please choose a valid number.") + except ValueError: + print("Invalid input. Please enter a number.") + + # Load and return map + print(f"Loading map: {choosen_map}") + return Map.load_from_file(os.path.join(MAPS_DIRECTORY, choosen_map)).map_to_dict() + + def start_server(host='127.0.0.1', port=5555): + + # Map + map_data = choose_map() + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((host, port)) server.listen() @@ -108,11 +143,13 @@ def start_server(host='127.0.0.1', port=5555): next_player_id += 1 # Initialize player's state - players_state[player_id] = {"px": 150, "py": 400, "pa": 90} + players_state[player_id] = {"px": 150, "py": 400, "pa": 90} # [ ] TODO: make a spawnpoint on map as a cell # Start handler - thread = threading.Thread(target=handle_client, args=(conn, addr, player_id), daemon=True) + thread = threading.Thread(target=handle_client, args=(conn, addr, player_id, map_data), daemon=True) thread.start() if __name__ == "__main__": + # Server Map Definition + start_server() From 3cfaf2add356967d04a7dfb3b7711b4e815bcfcd Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 19:35:06 +0100 Subject: [PATCH 24/45] feat: MapEditor: initial commit for the map editor --- mapeditor/MapEditor.py | 246 +++++++++++++++++++++++++++++++++++++++++ mapeditor/__init__.py | 1 + 2 files changed, 247 insertions(+) create mode 100644 mapeditor/MapEditor.py create mode 100644 mapeditor/__init__.py diff --git a/mapeditor/MapEditor.py b/mapeditor/MapEditor.py new file mode 100644 index 0000000..402ed65 --- /dev/null +++ b/mapeditor/MapEditor.py @@ -0,0 +1,246 @@ +import tkinter as tk +from tkinter import filedialog, messagebox +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 + + # Available textures and their display colors. + # For now, "0" is void and "1" is wall. + # When loading a file, if a colormap "wall" is found it will be used for texture "1". + self.textures = ["0", "1"] + self.texture_colors = {"0": "black", "1": "#db4900"} + + self.current_texture = "1" # Default painting texture + + self.create_widgets() + + def create_widgets(self): + # --- Left frame: Folder selection and file list --- + self.left_frame = tk.Frame(self.master) + self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5) + + self.btn_select_folder = tk.Button(self.left_frame, text="Select Folder", command=self.select_folder) + self.btn_select_folder.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) + + # --- Right frame: Canvas and controls --- + self.right_frame = tk.Frame(self.master) + self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) + + # Canvas for drawing the grid + self.canvas = tk.Canvas(self.right_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) + + # Control panel at the bottom + self.control_frame = tk.Frame(self.right_frame) + self.control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5) + + # Slider for map width + 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) + + # Slider for map height + 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) + + # Slider for tile size (mapS) + 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) + + # Texture selection (radio buttons) + self.texture_var = tk.StringVar(value=self.current_texture) + for tex in self.textures: + rb = tk.Radiobutton(self.control_frame, text=f"Texture {tex}", + variable=self.texture_var, value=tex, command=self.on_texture_change) + rb.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 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 grid list to 2D list (row-major) + 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 colormap if available + colorMap = data.get("colorMap", {}) + if "wall" in colorMap: + # Replace the "wall" texture with "1" + self.texture_colors["1"] = rgb_to_hex(colorMap["wall"]) + 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]) + # Optionally, update the canvas scroll region + 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}") + + 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 canvas_click(self, event): + self.paint_at(event) + + def canvas_drag(self, event): + self.paint_at(event) + + def paint_at(self, event): + row, col = self.get_cell_from_coords(event) + if row is not None and col is not None: + new_val = int(self.texture_var.get()) + if self.grid_data[row][col] != new_val: + self.grid_data[row][col] = new_val + # Redraw just this cell + 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_texture_change(self): + self.current_texture = self.texture_var.get() + + 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: + # Increase width by appending zeros to each row. + # This is equivalent to inserting a new column of 0s at the right edge. + self.grid_data[i].extend([0] * (new_width - old_width)) + elif new_width < old_width: + # Reduce width by slicing off extra cells from each row. + self.grid_data[i] = self.grid_data[i][:new_width] + 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: + # Add new rows (each row is a list of zeros) + for _ in range(new_height - old_height): + self.grid_data.append([0] * cols) + elif new_height < old_height: + # Remove rows from the bottom + self.grid_data = self.grid_data[:new_height] + self.map_data["mapY"] = new_height + self.draw_grid() + + def on_tile_size_change(self, value): + self.tile_size = int(value) + self.map_data["mapS"] = self.tile_size + self.draw_grid() + + 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 + # Update dimensions + self.map_data["mapX"] = len(self.grid_data[0]) + self.map_data["mapY"] = len(self.grid_data) + self.map_data["mapS"] = self.tile_size + + # Update the colormap: rename "wall" key to "1" if it exists. + if "colorMap" in self.map_data and "wall" in self.map_data["colorMap"]: + self.map_data["colorMap"]["1"] = self.map_data["colorMap"].pop("wall") + 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 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 From 3f6b9669e456e9243f9f98631944582e42faa3ab Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Wed, 19 Mar 2025 19:40:49 +0100 Subject: [PATCH 25/45] feat: MapEditor: add color pickers for textures --- mapeditor/MapEditor.py | 144 ++++++++++++++++++++++++++++++----------- 1 file changed, 107 insertions(+), 37 deletions(-) diff --git a/mapeditor/MapEditor.py b/mapeditor/MapEditor.py index 402ed65..2ca0a09 100644 --- a/mapeditor/MapEditor.py +++ b/mapeditor/MapEditor.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import filedialog, messagebox +from tkinter import filedialog, messagebox, simpledialog, colorchooser import json import os @@ -19,40 +19,46 @@ def __init__(self, master): self.grid_data = [] # 2D list representation of grid self.tile_size = 32 # Default tile size - # Available textures and their display colors. - # For now, "0" is void and "1" is wall. - # When loading a file, if a colormap "wall" is found it will be used for texture "1". + # Define textures (tiles). In our case, "0" (void) and "1" (wall). self.textures = ["0", "1"] - self.texture_colors = {"0": "black", "1": "#db4900"} + # Default colors. They can be changed via the right sidebar. + self.texture_colors = {"0": "black", "1": "#db4900"} # default for wall - self.current_texture = "1" # Default painting texture + self.current_texture = "1" # Default texture for painting - self.create_widgets() - - def create_widgets(self): - # --- Left frame: Folder selection and file list --- + # Set up three main frames: left (file management), center (canvas and controls), + # and right (texture editing). 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) - # --- Right frame: Canvas and controls --- - self.right_frame = tk.Frame(self.master) - self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) - - # Canvas for drawing the grid - self.canvas = tk.Canvas(self.right_frame, bg="gray") + # --- 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) - # Control panel at the bottom - self.control_frame = tk.Frame(self.right_frame) + self.control_frame = tk.Frame(self.center_frame) self.control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5) # Slider for map width @@ -71,17 +77,40 @@ def create_widgets(self): self.tile_size_slider.set(self.tile_size) self.tile_size_slider.pack(side=tk.LEFT, padx=5) - # Texture selection (radio buttons) + # Texture selection radio buttons (for painting) self.texture_var = tk.StringVar(value=self.current_texture) for tex in self.textures: rb = tk.Radiobutton(self.control_frame, text=f"Texture {tex}", - variable=self.texture_var, value=tex, command=self.on_texture_change) + variable=self.texture_var, value=tex, + command=self.on_texture_change) rb.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): + """Create the right sidebar for editing texture colors.""" + # Clear previous widgets in 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 tex in self.textures: + frame = tk.Frame(self.texture_frame) + frame.pack(pady=2, padx=5, fill=tk.X) + tk.Label(frame, text=f"Texture {tex}:").pack(side=tk.LEFT) + 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) + + 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() # update canvas + self.create_texture_sidebar() # refresh sidebar buttons to show new color + def select_folder(self): folder = filedialog.askdirectory() if folder: @@ -119,11 +148,16 @@ def load_file(self, file_path): self.width_slider.set(mapX) self.height_slider.set(mapY) - # Update texture colors from the colormap if available - colorMap = data.get("colorMap", {}) - if "wall" in colorMap: - # Replace the "wall" texture with "1" - self.texture_colors["1"] = rgb_to_hex(colorMap["wall"]) + # Update texture colors from the file's colormap. + # Expect the keys to match texture names (like "1"); if not, check for "wall". + if "colorMap" in data: + for key, rgb in data["colorMap"].items(): + self.texture_colors[key] = rgb_to_hex(rgb) + # Ensure defaults for any missing texture keys. + for tex in self.textures: + if tex not in self.texture_colors: + self.texture_colors[tex] = "gray" + self.create_texture_sidebar() self.draw_grid() except Exception as e: messagebox.showerror("Error", f"Failed to load file: {e}") @@ -134,7 +168,6 @@ def draw_grid(self): return rows = len(self.grid_data) cols = len(self.grid_data[0]) - # Optionally, update the canvas scroll region self.canvas.config(scrollregion=(0, 0, cols * self.tile_size, rows * self.tile_size)) for i in range(rows): for j in range(cols): @@ -187,12 +220,12 @@ def on_width_change(self, value): for i in range(rows): if new_width > old_width: # Increase width by appending zeros to each row. - # This is equivalent to inserting a new column of 0s at the right edge. self.grid_data[i].extend([0] * (new_width - old_width)) elif new_width < old_width: - # Reduce width by slicing off extra cells from each row. + # Reduce width by slicing off extra cells. self.grid_data[i] = self.grid_data[i][:new_width] - self.map_data["mapX"] = new_width + if self.map_data is not None: + self.map_data["mapX"] = new_width self.draw_grid() def on_height_change(self, value): @@ -202,18 +235,18 @@ def on_height_change(self, value): old_height = len(self.grid_data) cols = len(self.grid_data[0]) if new_height > old_height: - # Add new rows (each row is a list of zeros) for _ in range(new_height - old_height): self.grid_data.append([0] * cols) elif new_height < old_height: - # Remove rows from the bottom self.grid_data = self.grid_data[:new_height] - self.map_data["mapY"] = 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) - self.map_data["mapS"] = self.tile_size + if self.map_data is not None: + self.map_data["mapS"] = self.tile_size self.draw_grid() def save_file(self): @@ -222,14 +255,22 @@ def save_file(self): # 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 - # Update dimensions self.map_data["mapX"] = len(self.grid_data[0]) self.map_data["mapY"] = len(self.grid_data) self.map_data["mapS"] = self.tile_size - # Update the colormap: rename "wall" key to "1" if it exists. - if "colorMap" in self.map_data and "wall" in self.map_data["colorMap"]: - self.map_data["colorMap"]["1"] = self.map_data["colorMap"].pop("wall") + # Update the colormap so that keys match texture names. + if "colorMap" in self.map_data: + # Remove any old keys not in textures. + new_colorMap = {} + for tex in self.textures: + # Save the hex color as an RGB list. + hex_color = self.texture_colors.get(tex, "#000000") + 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) @@ -237,6 +278,35 @@ def save_file(self): 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 + # Create a default map: 16x16 grid of 0's, tile size 32, and a default colormap. + default_map = { + "grid": [0] * (16 * 16), + "mapX": 16, + "mapY": 16, + "mapS": 32, + "colorMap": {"1": [219, 73, 0]} + } + 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) From fadcc55247c5eafd02411b8157ad84159bac402d Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Thu, 20 Mar 2025 15:02:24 +0100 Subject: [PATCH 26/45] feat: MapEditor: improve texture picker --- mapeditor/MapEditor.py | 242 ++++++++++++++++++++++++++--------------- raycaster/Renderer.py | 2 +- 2 files changed, 158 insertions(+), 86 deletions(-) diff --git a/mapeditor/MapEditor.py b/mapeditor/MapEditor.py index 2ca0a09..1fa2f03 100644 --- a/mapeditor/MapEditor.py +++ b/mapeditor/MapEditor.py @@ -11,7 +11,7 @@ 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 @@ -19,15 +19,28 @@ def __init__(self, master): self.grid_data = [] # 2D list representation of grid self.tile_size = 32 # Default tile size - # Define textures (tiles). In our case, "0" (void) and "1" (wall). - self.textures = ["0", "1"] - # Default colors. They can be changed via the right sidebar. - self.texture_colors = {"0": "black", "1": "#db4900"} # default for wall - - self.current_texture = "1" # Default texture for painting - - # Set up three main frames: left (file management), center (canvas and controls), - # and right (texture editing). + # Define textures: + # Non-paintable textures (changeable via color picker but not used for painting) + self.non_paintable_textures = ["ground", "sky"] + # Paintable textures (these will 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. + # Note: "0" is our erase texture (default white), "ground" and "sky" come from your sample, + # and "1" is the default wall texture. + self.texture_colors = { + "0": "#ffffff", # erase / void (default white) + "ground": "#4ac22c", # from [74, 194, 44] + "sky": "#ebfffe", # from [235, 255, 254] + "1": "#db4900" # default for wall + } + + # Selected painting texture (only one among the paintable ones is used for painting) + 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) @@ -36,7 +49,7 @@ def __init__(self, master): 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() @@ -44,85 +57,100 @@ 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.control_frame = tk.Frame(self.center_frame) self.control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5) - - # Slider for map width + + # 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) - - # Slider for map height + 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) - - # Slider for tile size (mapS) + 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) - - # Texture selection radio buttons (for painting) - self.texture_var = tk.StringVar(value=self.current_texture) - for tex in self.textures: - rb = tk.Radiobutton(self.control_frame, text=f"Texture {tex}", - variable=self.texture_var, value=tex, - command=self.on_texture_change) - rb.pack(side=tk.LEFT, padx=5) - - # Save button + + # 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): - """Create the right sidebar for editing texture colors.""" - # Clear previous widgets in texture_frame + """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 tex in self.textures: + + # 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) - tk.Label(frame, text=f"Texture {tex}:").pack(side=tk.LEFT) + # If texture is paintable (i.e. 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: + # For non-paintable textures, simply show the label. + 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() # update canvas - self.create_texture_sidebar() # refresh sidebar buttons to show new 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.""" + # Determine next available number among painting textures. + 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 (128,128,128) + 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 @@ -130,15 +158,15 @@ def on_file_select(self, event): 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 grid list to 2D list (row-major) + + # 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", []) @@ -147,21 +175,55 @@ def load_file(self, file_path): 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. - # Expect the keys to match texture names (like "1"); if not, check for "wall". + # Remap known keys: "wall" -> "1"; "void"/"erase" -> "0". if "colorMap" in data: - for key, rgb in data["colorMap"].items(): - self.texture_colors[key] = rgb_to_hex(rgb) - # Ensure defaults for any missing texture keys. - for tex in self.textures: + 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] = "gray" + self.texture_colors[tex] = defaults.get(tex, "#000000") + # Also ensure defaults for our basic paintable textures. + 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 and not in non-paintable. + 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 the erase texture "0" is present. + if "0" not in self.paintable_textures: + self.texture_colors["0"] = self.texture_colors.get("0", "#ffffff") + self.paintable_textures.insert(0, "0") + # Ensure at least "1" is present. + 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]) + 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: @@ -175,31 +237,32 @@ def draw_grid(self): y1 = i * self.tile_size x2 = x1 + self.tile_size y2 = y1 + self.tile_size + # The grid stores numeric values for painting textures. 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}") - + 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 canvas_click(self, event): self.paint_at(event) - + def canvas_drag(self, event): self.paint_at(event) - + def paint_at(self, event): row, col = self.get_cell_from_coords(event) - if row is not None and col is not None: + if row is not None and col is not None and self.texture_var.get(): + # Use the selected painting texture (a digit string). new_val = int(self.texture_var.get()) if self.grid_data[row][col] != new_val: self.grid_data[row][col] = new_val - # Redraw just this cell x1 = col * self.tile_size y1 = row * self.tile_size x2 = x1 + self.tile_size @@ -207,10 +270,7 @@ def paint_at(self, event): 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_texture_change(self): - self.current_texture = self.texture_var.get() - + def on_width_change(self, value): if not self.grid_data: return @@ -219,15 +279,13 @@ def on_width_change(self, value): rows = len(self.grid_data) for i in range(rows): if new_width > old_width: - # Increase width by appending zeros to each row. self.grid_data[i].extend([0] * (new_width - old_width)) elif new_width < old_width: - # Reduce width by slicing off extra cells. 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 @@ -242,42 +300,51 @@ def on_height_change(self, value): 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 + # 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 - - # Update the colormap so that keys match texture names. - if "colorMap" in self.map_data: - # Remove any old keys not in textures. - new_colorMap = {} - for tex in self.textures: - # Save the hex color as an RGB list. - hex_color = self.texture_colors.get(tex, "#000000") - 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 - + + # 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.") @@ -291,13 +358,18 @@ def add_map(self): if os.path.exists(file_path): messagebox.showwarning("File Exists", "File already exists!") return - # Create a default map: 16x16 grid of 0's, tile size 32, and a default colormap. + # Default map: 16x16 grid, tile size 32, with a default colormap. default_map = { "grid": [0] * (16 * 16), "mapX": 16, "mapY": 16, "mapS": 32, - "colorMap": {"1": [219, 73, 0]} + "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: diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 5e5aee5..74d81ae 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -96,7 +96,7 @@ def cast_rays(self, player): if line_height > 320: line_height = 320 line_offset = 160 - line_height / 2 - base_color = self.map.get_color("wall") + base_color = self.map.get_color("1") # Control how fast the damping applies: distance_factor = max(0.4, 1 - dis / max_distance) # Keep at least 40% brightness From bfe540b9b9d0a344733fa3865158fa996e2bf955 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Thu, 20 Mar 2025 15:04:57 +0100 Subject: [PATCH 27/45] feat(maps): new maps --- maps/mansion.json | 290 ++++++++++++++++++++++++++++++++++++++++++++++ maps/test.json | 221 +++++++++++++++++++++++++++++++++++ 2 files changed, 511 insertions(+) create mode 100644 maps/mansion.json create mode 100644 maps/test.json diff --git a/maps/mansion.json b/maps/mansion.json new file mode 100644 index 0000000..35beb38 --- /dev/null +++ b/maps/mansion.json @@ -0,0 +1,290 @@ +{ + "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 + ] + } +} \ No newline at end of file diff --git a/maps/test.json b/maps/test.json new file mode 100644 index 0000000..37e04d7 --- /dev/null +++ b/maps/test.json @@ -0,0 +1,221 @@ +{ + "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 + ] + } +} \ No newline at end of file From bb6b4bcdd56890dac3d8ca52e66f67e085a891b3 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Thu, 20 Mar 2025 16:18:45 +0100 Subject: [PATCH 28/45] feat!: adding spawnpoint to maps & MapEditor --- main.py | 4 +- mapeditor/MapEditor.py | 104 +++++++++++++++++++++++++++++------------ maps/mansion.json | 6 ++- raycaster/Map.py | 14 ++++-- server_network.py | 5 +- 5 files changed, 94 insertions(+), 39 deletions(-) diff --git a/main.py b/main.py index 69397a5..43b4c2f 100644 --- a/main.py +++ b/main.py @@ -15,11 +15,11 @@ print("[CLIENT] Waiting for 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_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': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'} -player = Player(150, 400, 90, key_bindings, map_obj) +player = Player(map_info["spawnpoint"][0], map_info["spawnpoint"][1], 90, key_bindings, map_obj) renderer = Renderer(map_obj, [player]) # Local player only for now diff --git a/mapeditor/MapEditor.py b/mapeditor/MapEditor.py index 1fa2f03..e1237bc 100644 --- a/mapeditor/MapEditor.py +++ b/mapeditor/MapEditor.py @@ -18,26 +18,29 @@ def __init__(self, master): 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 (changeable via color picker but not used for painting) + # Non-paintable textures (only color adjustable, not used for painting) self.non_paintable_textures = ["ground", "sky"] - # Paintable textures (these will appear as selectable radio buttons) + # 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. - # Note: "0" is our erase texture (default white), "ground" and "sky" come from your sample, - # and "1" is the default wall texture. + # "0" (erase) is white, "ground" and "sky" come from your sample, "1" is wall. self.texture_colors = { - "0": "#ffffff", # erase / void (default white) + "0": "#ffffff", # erase/void (white) "ground": "#4ac22c", # from [74, 194, 44] "sky": "#ebfffe", # from [235, 255, 254] - "1": "#db4900" # default for wall + "1": "#db4900" # default wall } - # Selected painting texture (only one among the paintable ones is used for painting) + # 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) @@ -70,6 +73,7 @@ def create_widgets(self): 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) @@ -104,12 +108,11 @@ def create_texture_sidebar(self): for tex in self.all_textures: frame = tk.Frame(self.texture_frame) frame.pack(pady=2, padx=5, fill=tk.X) - # If texture is paintable (i.e. not ground or sky), add a radio button. + # 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: - # For non-paintable textures, simply show the label. 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, @@ -130,13 +133,12 @@ def choose_color(self, tex): def add_texture(self): """Adds a new paintable texture with the next available number and default gray color.""" - # Determine next available number among painting textures. 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 (128,128,128) + self.texture_colors[next_texture] = "#808080" # default gray self.create_texture_sidebar() def select_folder(self): @@ -177,7 +179,7 @@ def load_file(self, file_path): self.height_slider.set(mapY) # Update texture colors from the file's colormap. - # Remap known keys: "wall" -> "1"; "void"/"erase" -> "0". + # Remap keys: "wall" -> "1"; "void"/"erase" -> "0". if "colorMap" in data: file_colormap = data["colorMap"] for key, rgb in file_colormap.items(): @@ -193,22 +195,20 @@ def load_file(self, file_path): for tex in self.non_paintable_textures: if tex not in self.texture_colors: self.texture_colors[tex] = defaults.get(tex, "#000000") - # Also ensure defaults for our basic paintable textures. 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 and not in non-paintable. + + # 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 the erase texture "0" is present. + # 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") - # Ensure at least "1" is present. if "1" not in self.paintable_textures: self.texture_colors["1"] = self.texture_colors.get("1", "#db4900") self.paintable_textures.append("1") @@ -218,7 +218,14 @@ def load_file(self, file_path): # 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: @@ -237,12 +244,54 @@ def draw_grid(self): y1 = i * self.tile_size x2 = x1 + self.tile_size y2 = y1 + self.tile_size - # The grid stores numeric values for painting textures. 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 @@ -250,16 +299,9 @@ def get_cell_from_coords(self, event): return None, None return row, col - def canvas_click(self, event): - self.paint_at(event) - - def canvas_drag(self, event): - self.paint_at(event) - 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(): - # Use the selected painting texture (a digit string). new_val = int(self.texture_var.get()) if self.grid_data[row][col] != new_val: self.grid_data[row][col] = new_val @@ -270,7 +312,7 @@ def paint_at(self, event): 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 @@ -327,6 +369,9 @@ def save_file(self): 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 = {} @@ -358,12 +403,13 @@ def add_map(self): if os.path.exists(file_path): messagebox.showwarning("File Exists", "File already exists!") return - # Default map: 16x16 grid, tile size 32, with a default colormap. + # 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], diff --git a/maps/mansion.json b/maps/mansion.json index 35beb38..e853ec7 100644 --- a/maps/mansion.json +++ b/maps/mansion.json @@ -286,5 +286,9 @@ 210, 69 ] - } + }, + "spawnpoint": [ + 305, + 432 + ] } \ No newline at end of file diff --git a/raycaster/Map.py b/raycaster/Map.py index 59b1d12..1812d2c 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -2,12 +2,13 @@ import json class Map: - def __init__(self, grid, mapX, mapY, mapS, colorMap): + 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: @@ -36,7 +37,8 @@ def save_to_file(self, filename): "mapX": self.mapX, "mapY": self.mapY, "mapS": self.mapS, - "colorMap": self.colorMap + "colorMap": self.colorMap, + "spawnpoint": self.spawnpoint } with open(filename, 'w') as f: json.dump(map_data, f) @@ -47,7 +49,7 @@ def load_from_file(cls, filename): with open(filename, 'r') as f: map_data = json.load(f) print(f"[MAP] Loaded map from {filename}") - return cls(map_data["grid"], map_data["mapX"], map_data["mapY"], map_data["mapS"], map_data["colorMap"]) + 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 { @@ -55,7 +57,8 @@ def map_to_dict(self): "mapX": self.mapX, "mapY": self.mapY, "mapS": self.mapS, - "colorMap": self.colorMap + "colorMap": self.colorMap, + "spawnpoint": self.spawnpoint } def get_color(self, texture): @@ -66,6 +69,9 @@ def get_color(self, texture): print(f"[MAP] TEXTURE {texture} DOES NOT EXIST") return color + def get_spawnpoint(self): + return self.spawnpoint + diff --git a/server_network.py b/server_network.py index 2c5f21c..2129ef4 100644 --- a/server_network.py +++ b/server_network.py @@ -136,6 +136,7 @@ def start_server(host='127.0.0.1', port=5555): print(f"[SERVER] Listening on {host}:{port}") next_player_id = 1 + while True: conn, addr = server.accept() @@ -143,13 +144,11 @@ def start_server(host='127.0.0.1', port=5555): next_player_id += 1 # Initialize player's state - players_state[player_id] = {"px": 150, "py": 400, "pa": 90} # [ ] TODO: make a spawnpoint on map as a cell + players_state[player_id] = {"px": map_data["spawnpoint"][0], "py": map_data["spawnpoint"][1], "pa": 90} # Start handler thread = threading.Thread(target=handle_client, args=(conn, addr, player_id, map_data), daemon=True) thread.start() if __name__ == "__main__": - # Server Map Definition - start_server() From bd001fd146ed0a272e0e758fae0b30fc2c61c744 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Thu, 20 Mar 2025 22:05:06 +0100 Subject: [PATCH 29/45] feat(map/renderer): add multiple tile types --- raycaster/Map.py | 17 +++++++++-------- raycaster/Renderer.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/raycaster/Map.py b/raycaster/Map.py index 1812d2c..49b6e82 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -12,22 +12,23 @@ def __init__(self, grid, mapX, mapY, mapS, colorMap, spawnpoint): def is_wall(self, mx, my): if 0 <= mx < self.mapX and 0 <= my < self.mapY: - return self.grid[my * self.mapX + mx] == 1 + return self.grid[my * self.mapX + mx] != 0 return True # Out of bounds = wall def draw(self): for y in range(self.mapY): for x in range(self.mapX): - if self.grid[y * self.mapX + x] == 1: - glColor3f(1, 1, 1) + cell_value = self.grid[y * self.mapX + x] + if cell_value != 0 : # if not void + glColor3ub(*self.get_color(str(cell_value))) else: - glColor3f(0, 0, 0) + glColor3ub(*self.get_color(str("ground"))) xo, yo = x * self.mapS, y * self.mapS glBegin(GL_QUADS) - glVertex2i(xo + 1, yo + 1) - glVertex2i(xo + 1, yo + self.mapS - 1) - glVertex2i(xo + self.mapS - 1, yo + self.mapS - 1) - glVertex2i(xo + self.mapS - 1, yo + 1) + glVertex2i(xo, yo ) + glVertex2i(xo, yo + self.mapS) + glVertex2i(xo + self.mapS, yo + self.mapS) + glVertex2i(xo + self.mapS, yo ) glEnd() # TODO look into if i should use static method here and on other classes diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 74d81ae..d1816a7 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -46,6 +46,8 @@ def cast_rays(self, player): rx, ry = player.px, player.py hit_wall = False # Track if wall is hit + # Get collision data + while dof < 20: # Store previous grid cell BEFORE moving prev_mx = int(rx) // self.map.mapS @@ -60,10 +62,15 @@ def cast_rays(self, player): mx = int(rx) // self.map.mapS my = int(ry) // self.map.mapS + # If outside of map, stop if mx < 0 or mx >= self.map.mapX or my < 0 or my >= self.map.mapY: break - if self.map.grid[my * self.map.mapX + mx] == 1: + # Get wall hit + hit_cell_value = self.map.grid[my * self.map.mapX + mx] + + # If hit + if hit_cell_value != 0: dis = hypot(rx - player.px, ry - player.py) hit_wall = True @@ -96,7 +103,7 @@ def cast_rays(self, player): if line_height > 320: line_height = 320 line_offset = 160 - line_height / 2 - base_color = self.map.get_color("1") + base_color = self.map.get_color(str(hit_cell_value)) # Get color of wall # Control how fast the damping applies: distance_factor = max(0.4, 1 - dis / max_distance) # Keep at least 40% brightness From 4e25bde28bbd31b5551558987b7f43f90114f0a0 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Thu, 20 Mar 2025 22:34:59 +0100 Subject: [PATCH 30/45] new map --- maps/maze.json | 438 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 maps/maze.json 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 From c7c0664546c44bbc8136ed5c887e6f2367e98845 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Thu, 20 Mar 2025 23:17:17 +0100 Subject: [PATCH 31/45] feat: static minimap size & map getters --- raycaster/Map.py | 33 ++++++++++++++++++++++++++++----- raycaster/Player.py | 14 +++++++++++--- raycaster/Renderer.py | 3 ++- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/raycaster/Map.py b/raycaster/Map.py index 49b6e82..c2f5414 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -9,6 +9,7 @@ def __init__(self, grid, mapX, mapY, mapS, colorMap, spawnpoint): self.mapS = mapS # Tile size in pixels. Should be 32 to 128. Ideally 64 self.colorMap = colorMap self.spawnpoint = spawnpoint + self.minimap_size = 16 def is_wall(self, mx, my): if 0 <= mx < self.mapX and 0 <= my < self.mapY: @@ -23,12 +24,14 @@ def draw(self): glColor3ub(*self.get_color(str(cell_value))) else: glColor3ub(*self.get_color(str("ground"))) - xo, yo = x * self.mapS, y * self.mapS + + minimap_size = self.get_minimap_size() + xo, yo = x * minimap_size, y * minimap_size glBegin(GL_QUADS) - glVertex2i(xo, yo ) - glVertex2i(xo, yo + self.mapS) - glVertex2i(xo + self.mapS, yo + self.mapS) - glVertex2i(xo + self.mapS, yo ) + glVertex2i(xo, yo ) + glVertex2i(xo, yo + minimap_size ) + glVertex2i(xo + minimap_size, yo + minimap_size) + glVertex2i(xo + minimap_size, yo ) glEnd() # TODO look into if i should use static method here and on other classes @@ -62,6 +65,7 @@ def map_to_dict(self): "spawnpoint": self.spawnpoint } + # GETTERS def get_color(self, texture): try: color = self.colorMap[texture] @@ -73,7 +77,26 @@ def get_color(self, texture): 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 + + def get_minimap_size(self): + return self.minimap_size diff --git a/raycaster/Player.py b/raycaster/Player.py index 90d1699..e8388b2 100644 --- a/raycaster/Player.py +++ b/raycaster/Player.py @@ -48,13 +48,21 @@ def move(self, delta_time): def draw(self): + glColor3f(1, 1, 0) + + map_x = self.px / self.map.get_mapS() * self.map.get_minimap_size() + map_y = self.py / self.map.get_mapS() * self.map.get_minimap_size() + + # Dot glPointSize(8) glLineWidth(3) glBegin(GL_POINTS) - glVertex2i(int(self.px), int(self.py)) + glVertex2i(int(map_x), int(map_y)) glEnd() + + # Forward segment glBegin(GL_LINES) - glVertex2i(int(self.px), int(self.py)) - glVertex2i(int(self.px + 20 * self.pdx), int(self.py + 20 * self.pdy)) + glVertex2i(int(map_x), int(map_y)) + glVertex2i(int(map_x + 20 * self.pdx), int(map_y + 20 * self.pdy)) glEnd() diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index d1816a7..b13146d 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -9,9 +9,10 @@ def __init__(self, map_obj, players): self.num_rays = 60 def draw_scene(self, player): + + self.cast_rays(player) # Raycasting specific to this player self.map.draw() player.draw() - self.cast_rays(player) # Raycasting specific to this player def cast_rays(self, player): From ba1ffb7c1ae970ef42e722653676836185153e6d Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Fri, 21 Mar 2025 01:03:20 +0100 Subject: [PATCH 32/45] feat(renderer): add dynamic screen size --- main.py | 16 ++++-- raycaster/Map.py | 2 +- raycaster/Renderer.py | 124 +++++++++++++++++++++--------------------- 3 files changed, 75 insertions(+), 67 deletions(-) diff --git a/main.py b/main.py index 43b4c2f..15d50af 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,15 @@ last_time = time.time() +# === GLUT INITIALIZATION === +glutInit() +glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) +glutInitWindowSize(1024, 512) +glutCreateWindow(b"Networked Raycaster Client") + +glutReshapeFunc(renderer.reshape) + + # === INPUT HANDLERS === def keyboard_down(key, x, y): try: @@ -75,17 +84,14 @@ def display(): # print(f"[CLIENT] Rendering players_state: {network.players_state}") # DEBUG -# === GLUT INITIALIZATION === -glutInit() -glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) -glutInitWindowSize(1024, 512) -glutCreateWindow(b"Networked Raycaster Client") +# === 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) diff --git a/raycaster/Map.py b/raycaster/Map.py index c2f5414..eaedd47 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -9,7 +9,7 @@ def __init__(self, grid, mapX, mapY, mapS, colorMap, spawnpoint): self.mapS = mapS # Tile size in pixels. Should be 32 to 128. Ideally 64 self.colorMap = colorMap self.spawnpoint = spawnpoint - self.minimap_size = 16 + self.minimap_size = 4 def is_wall(self, mx, my): if 0 <= mx < self.mapX and 0 <= my < self.mapY: diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index b13146d..0c326f7 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -1,5 +1,7 @@ from math import sin, cos, radians, hypot from OpenGL.GL import * +from OpenGL.GLU import * +from OpenGL.GLUT import * class Renderer: def __init__(self, map_obj, players): @@ -8,35 +10,51 @@ def __init__(self, map_obj, players): self.FOV = 60 self.num_rays = 60 - def draw_scene(self, player): - self.cast_rays(player) # Raycasting specific to this player + 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): + self.cast_rays(player) self.map.draw() player.draw() - def cast_rays(self, player): - - max_distance = 1000 # For shading according to distance + max_distance = 1000 + + screen_width, screen_height = self.get_window_size() + slice_width = screen_width / self.num_rays - # Draws sky + # Draw sky sky_color = self.map.get_color("sky") glColor3ub(*sky_color) glBegin(GL_QUADS) - glVertex(526, 0) - glVertex(1006, 0) - glVertex(1006,160) - glVertex(526,160) + glVertex2i(0, 0) + glVertex2i(screen_width, 0) + glVertex2i(screen_width, screen_height // 2) + glVertex2i(0, screen_height // 2) glEnd() - #Draws floor + # Draw floor ground_color = self.map.get_color("ground") glColor3ub(*ground_color) glBegin(GL_QUADS) - glVertex2i(526,160) - glVertex2i(1006,160) - glVertex2i(1006,320) - glVertex2i(526,320) + 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) @@ -45,98 +63,82 @@ def cast_rays(self, player): dof = 0 dis = 0 rx, ry = player.px, player.py - hit_wall = False # Track if wall is hit - - # Get collision data + hit_wall = False while dof < 20: - # Store previous grid cell BEFORE moving prev_mx = int(rx) // self.map.mapS prev_my = int(ry) // self.map.mapS - # Move ray rx += cos(ray_angle) * 5 ry -= sin(ray_angle) * 5 dof += 0.1 - # Current grid cell mx = int(rx) // self.map.mapS my = int(ry) // self.map.mapS - # If outside of map, stop if mx < 0 or mx >= self.map.mapX or my < 0 or my >= self.map.mapY: break - # Get wall hit hit_cell_value = self.map.grid[my * self.map.mapX + mx] - - # If hit + if hit_cell_value != 0: dis = hypot(rx - player.px, ry - player.py) hit_wall = True - # Compare previous and current grid cells delta_mx = mx - prev_mx delta_my = my - prev_my if abs(delta_mx) > abs(delta_my): - if delta_mx > 0: - wall_face = "West" - else: - wall_face = "East" + wall_face = "West" if delta_mx > 0 else "East" else: - if delta_my > 0: - wall_face = "North" - else: - wall_face = "South" - + wall_face = "North" if delta_my > 0 else "South" break - if not hit_wall: ra -= (self.FOV / self.num_rays) - continue # Skip drawing wall slice + continue - # Wall hit, draw projection: + # Fisheye correction ca = radians(player.pa - ra) - dis *= cos(ca) # Fix fisheye - line_height = (self.map.mapS * 320) / dis # [ ] FIXME Potential division by 0 - if line_height > 320: - line_height = 320 - line_offset = 160 - line_height / 2 - base_color = self.map.get_color(str(hit_cell_value)) # Get color of wall + dis *= cos(ca) - # Control how fast the damping applies: - distance_factor = max(0.4, 1 - dis / max_distance) # Keep at least 40% brightness + # 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 / max_distance) ** 0.5) - # Shading shade_factors = { "North": 1.0, - "South": 0.6, - "East": 0.8, - "West": 0.9 + "South": 0.8, + "East": 0.7, + "West": 0.6 } - - # Wall face factor: face_factor = shade_factors.get(wall_face, 1.0) - - # Final factor: final_factor = face_factor * distance_factor - # Apply: shaded_color = tuple( max(0, min(255, int(c * final_factor))) for c in base_color ) - # Change color - glColor3ub(*shaded_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) - # Draw vertical line according to ray - glLineWidth(8) + glColor3ub(*shaded_color) glBegin(GL_LINES) - glVertex2i(r * 8 + 530, int(line_offset)) - glVertex2i(r * 8 + 530, int(line_offset + line_height)) + glVertex2i(x, int(line_offset)) + glVertex2i(x, int(line_offset + line_height)) glEnd() ra -= (self.FOV / self.num_rays) From 2dd1cc64a58423b3f2ee5e2f704d593d5e216a42 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Fri, 21 Mar 2025 01:32:45 +0100 Subject: [PATCH 33/45] remove default map creation in Map.py --- raycaster/Map.py | 103 +---------------------------------------------- 1 file changed, 1 insertion(+), 102 deletions(-) diff --git a/raycaster/Map.py b/raycaster/Map.py index eaedd47..eb717e3 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -97,105 +97,4 @@ def get_spawnpoint(self): def get_minimap_size(self): return self.minimap_size - - - -if __name__ == "__main__": - """ - Test of the class and its methods - """ - - 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 - ] - - colorMap = { - "ground":(74,194,44), - "sky":(235,255,254), - "wall":(100,100,100) - } - - map_info = { - "grid": world, - "mapX": 8, - "mapY": 8, - "mapS": 64, - "colorMap":colorMap - } - - map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info['colorMap']) - - map.save_to_file('maps/aryantech123.json') - - # MAP 2 - - world = [ - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - ] - - map_info = { - "grid": world, - "mapX": 16, - "mapY": 16, - "mapS": 64, - "colorMap":colorMap - } - - map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info['colorMap']) - - map.save_to_file('maps/empty_l.json') - - # MAP 3 - - world = [ - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,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 - ] - - map_info = { - "grid": world, - "mapX": 16, - "mapY": 16, - "mapS": 32, - "colorMap":colorMap - } - - map = Map(map_info['grid'], map_info['mapX'], map_info['mapY'], map_info['mapS'], map_info['colorMap']) - - map.save_to_file('maps/house_l.json') \ No newline at end of file + \ No newline at end of file From 6d4d2ec4235b98505ee27c1fa0c45c794559b771 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Fri, 21 Mar 2025 11:36:34 +0100 Subject: [PATCH 34/45] feat(player): add config file for render/keybinds --- .env | 10 ++++++++++ TODO | 6 ++---- main.py | 32 ++++++++++++++++++++++++++++---- raycaster/Renderer.py | 41 +++++++++++++++++++++++++++++------------ 4 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..cbde409 --- /dev/null +++ b/.env @@ -0,0 +1,10 @@ +# RENDERING OPTIONS +RAYCASTER_FOV=70 # 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, + +# KEY BINDS +KEY_FORWARD=z +KEY_LEFT=q +KEY_BACKWARD=s +KEY_RIGHT=d \ No newline at end of file diff --git a/TODO b/TODO index 04a02a1..016f0f5 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,2 @@ -[ ] Create a map manager/editor -[ ] Detect keyboard layout for key bindings -[ ] Add different kind of walls -[ ] Use texture mapping \ No newline at end of file +[ ] Use texture mapping +[ ] FIXME: multiplayer minimap still follows old behavior \ No newline at end of file diff --git a/main.py b/main.py index 15d50af..ad45d47 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,27 @@ from multiplayer import ClientNetwork import time import threading +from dotenv import load_dotenv +import os + +load_dotenv() + +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') # === NETWORK INITIALIZATION === network = ClientNetwork() # Connects to server at localhost:5555 by default @@ -18,13 +39,14 @@ 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': 'z', 'BACKWARD': 's', 'LEFT': 'q', 'RIGHT': 'd'} +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, [player]) # Local player only for now +renderer = Renderer(map_obj=map_obj, fov=FOV, num_rays=NUM_RAYS, max_distance=MAX_DISTANCE) # Local player only for now last_time = time.time() + # === GLUT INITIALIZATION === glutInit() glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) @@ -37,7 +59,7 @@ # === INPUT HANDLERS === def keyboard_down(key, x, y): try: - key_char = key.decode() + key_char = key.decode().lower() if key_char in player.key_bindings.values(): player.keys_pressed.add(key_char) except UnicodeDecodeError: @@ -45,12 +67,13 @@ def keyboard_down(key, x, y): def keyboard_up(key, x, y): try: - key_char = key.decode() + 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(): global last_time @@ -102,4 +125,5 @@ def on_close(): import atexit atexit.register(on_close) +# === MAIN LOOP === glutMainLoop() diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 0c326f7..62757cd 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -4,11 +4,11 @@ from OpenGL.GLUT import * class Renderer: - def __init__(self, map_obj, players): + def __init__(self, map_obj, fov = 80, num_rays = 120, max_distance = 500): self.map = map_obj - self.players = players - self.FOV = 60 - self.num_rays = 60 + self.FOV = fov + self.num_rays = num_rays # resolution + self.max_distance = max_distance def reshape(self, width, height): @@ -32,7 +32,6 @@ def draw_scene(self, player): player.draw() def cast_rays(self, player): - max_distance = 1000 screen_width, screen_height = self.get_window_size() slice_width = screen_width / self.num_rays @@ -66,20 +65,20 @@ def cast_rays(self, player): hit_wall = False while dof < 20: - prev_mx = int(rx) // self.map.mapS - prev_my = int(ry) // self.map.mapS + 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.mapS - my = int(ry) // self.map.mapS + mx = int(rx) // self.map.get_mapS() + my = int(ry) // self.map.get_mapS() - if mx < 0 or mx >= self.map.mapX or my < 0 or my >= self.map.mapY: + if mx < 0 or mx >= self.map.get_mapX() or my < 0 or my >= self.map.get_mapY(): break - hit_cell_value = self.map.grid[my * self.map.mapX + mx] + 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) @@ -110,7 +109,7 @@ def cast_rays(self, player): # Wall color and shading base_color = self.map.get_color(str(hit_cell_value)) - distance_factor = max(0.7, 1 - (dis / max_distance) ** 0.5) + distance_factor = max(0.7, 1 - (dis / self.get_max_distance()) ** 0.5) shade_factors = { "North": 1.0, @@ -142,3 +141,21 @@ def cast_rays(self, player): glEnd() ra -= (self.FOV / self.num_rays) + + # 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 + + + + From a9c314d8f0b9bff459300759fdcf7bad798fb0d4 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Fri, 21 Mar 2025 14:50:19 +0100 Subject: [PATCH 35/45] fix: multiplayer minimap followed old behavior --- .env | 9 +++++- TODO | 3 +- main.py | 13 +++++---- raycaster/Map.py | 21 ------------- raycaster/Player.py | 44 ++++++++++++++++------------ raycaster/Renderer.py | 68 +++++++++++++++++++++++++++++++++++++++---- server_network.py | 2 +- 7 files changed, 105 insertions(+), 55 deletions(-) diff --git a/.env b/.env index cbde409..5b53d69 100644 --- a/.env +++ b/.env @@ -1,8 +1,15 @@ -# RENDERING OPTIONS +# === GRAPHIC OPTIONS === + +# - RENDERING OPTIONS - RAYCASTER_FOV=70 # 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=6 + +# === CONTROLS === + # KEY BINDS KEY_FORWARD=z KEY_LEFT=q diff --git a/TODO b/TODO index 016f0f5..e7862f6 100644 --- a/TODO +++ b/TODO @@ -1,2 +1 @@ -[ ] Use texture mapping -[ ] FIXME: multiplayer minimap still follows old behavior \ No newline at end of file +[ ] Use texture mapping \ No newline at end of file diff --git a/main.py b/main.py index ad45d47..4a7113e 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,7 @@ def get_env_str(key, default): 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) # === NETWORK INITIALIZATION === network = ClientNetwork() # Connects to server at localhost:5555 by default @@ -42,7 +43,7 @@ def get_env_str(key, default): 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) # Local player only for now +renderer = Renderer(map_obj=map_obj, fov=FOV, num_rays=NUM_RAYS, max_distance=MAX_DISTANCE, minimap_size=MAP_MINIMAP_SIZE) # Local player only for now last_time = time.time() @@ -93,17 +94,17 @@ def display(): renderer.draw_scene(player) # Draw remote players (optional, if desired) - glColor3f(0.0, 1.0, 0.0) # Different color for remote 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 - glPointSize(6) - glBegin(GL_POINTS) - glVertex2i(int(remote_data['px']), int(remote_data['py'])) - glEnd() + px = int(remote_data['px']) + py = int(remote_data['py']) + pa = int(remote_data['pa']) + renderer.draw_minimap_player((px,py,pa)) glutSwapBuffers() + # print(f"[CLIENT] Rendering players_state: {network.players_state}") # DEBUG diff --git a/raycaster/Map.py b/raycaster/Map.py index eb717e3..1a7889a 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -9,31 +9,12 @@ def __init__(self, grid, mapX, mapY, mapS, colorMap, spawnpoint): self.mapS = mapS # Tile size in pixels. Should be 32 to 128. Ideally 64 self.colorMap = colorMap self.spawnpoint = spawnpoint - self.minimap_size = 4 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 - def draw(self): - for y in range(self.mapY): - for x in range(self.mapX): - cell_value = self.grid[y * self.mapX + x] - if cell_value != 0 : # if not void - glColor3ub(*self.get_color(str(cell_value))) - else: - glColor3ub(*self.get_color(str("ground"))) - - minimap_size = self.get_minimap_size() - 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() - # TODO look into if i should use static method here and on other classes def save_to_file(self, filename): map_data = { @@ -95,6 +76,4 @@ def get_colorMap(self): def get_spawnpoint(self): return self.spawnpoint - def get_minimap_size(self): - return self.minimap_size \ No newline at end of file diff --git a/raycaster/Player.py b/raycaster/Player.py index e8388b2..98a7f62 100644 --- a/raycaster/Player.py +++ b/raycaster/Player.py @@ -46,23 +46,29 @@ def move(self, delta_time): self.px = new_x self.py = new_y + # Getters + def get_px(self): + return self.px - def draw(self): - - glColor3f(1, 1, 0) - - map_x = self.px / self.map.get_mapS() * self.map.get_minimap_size() - map_y = self.py / self.map.get_mapS() * self.map.get_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 + 20 * self.pdx), int(map_y + 20 * self.pdy)) - glEnd() + 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 index 62757cd..5bbbef5 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -1,15 +1,17 @@ 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 = 80, num_rays = 120, max_distance = 500): + def __init__(self, map_obj, fov:int = 80, num_rays = 120, max_distance = 500, minimap_size = 4): self.map = map_obj self.FOV = fov self.num_rays = num_rays # resolution - self.max_distance = max_distance - + self.max_distance = max_distance # shading + self.minimap_size = minimap_size def reshape(self, width, height): """Handles window resizing correctly.""" @@ -28,8 +30,8 @@ def get_window_size(self): def draw_scene(self, player): self.cast_rays(player) - self.map.draw() - player.draw() + self.draw_minimap() + self.draw_minimap_player(player) def cast_rays(self, player): @@ -141,7 +143,61 @@ def cast_rays(self, player): glEnd() ra -= (self.FOV / self.num_rays) + + def draw_minimap(self): + minimap_size = self.get_minimap_size() + 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 + glColor3ub(*self.map.get_color(str(cell_value))) + else: + glColor3ub(*self.map.get_color(str("ground"))) + + 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() + segment_length = 0.25 + + if isinstance(player_or_coords, Player): + glColor3f(0, 1, 1) # 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: + glColor3f(1, 0, 0) # 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 + 10 * pdx), int(map_y + 10 * pdy)) + glEnd() + # Getters def get_map(self): @@ -156,6 +212,8 @@ def get_num_rays(self): def get_max_distance(self): return self.max_distance + def get_minimap_size(self): + return self.minimap_size diff --git a/server_network.py b/server_network.py index 2129ef4..37eb7d0 100644 --- a/server_network.py +++ b/server_network.py @@ -26,7 +26,7 @@ def recv_exact(sock, n): return data def broadcast(): - # print(f"[SERVER] Broadcasting: {players_state}") # DEBUG + print(f"[SERVER] Broadcasting: {players_state}") # DEBUG for conn in list(clients.keys()): try: send_message(conn, players_state) From f224446487f9ffa78ed71e781326aa66c0e8b4b1 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Fri, 21 Mar 2025 14:51:37 +0100 Subject: [PATCH 36/45] new map --- maps/maze2.json | 438 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 maps/maze2.json 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 From b2d7d7fbd67351eba9f592aa03532081a5c65b76 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Fri, 21 Mar 2025 17:47:10 +0100 Subject: [PATCH 37/45] feat(minimap): add map opacity option --- .env | 5 +++-- .gitignore | 3 ++- main.py | 21 ++++++++++++++++++++- raycaster/Renderer.py | 26 ++++++++++++++------------ 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.env b/.env index 5b53d69..db55431 100644 --- a/.env +++ b/.env @@ -1,12 +1,13 @@ # === GRAPHIC OPTIONS === # - RENDERING OPTIONS - -RAYCASTER_FOV=70 # Field Of View in degrees (°) Keep between 60 and 110 for better rendering +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=6 +MAP_MINIMAP_SIZE=12 +MAP_MINIMAP_OPACITY=210 # range from 0 to 255 # === CONTROLS === diff --git a/.gitignore b/.gitignore index ed8ebf5..37bbdde 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__ \ No newline at end of file +__pycache__ +temp/ \ No newline at end of file diff --git a/main.py b/main.py index 4a7113e..f6e935f 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,8 @@ +############################################################################### +# IMPORTS # +############################################################################### +from alive_progress import alive_bar + from raycaster import Map, Player, Renderer from OpenGL.GL import * from OpenGL.GLU import * @@ -8,6 +13,11 @@ from dotenv import load_dotenv import os + +############################################################################### +# CONSTANTS # +############################################################################### + load_dotenv() def get_env_int(key, default): @@ -27,6 +37,12 @@ def get_env_str(key, default): 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 @@ -43,7 +59,7 @@ def get_env_str(key, default): 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) # Local player only for now +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() @@ -54,6 +70,9 @@ def get_env_str(key, default): glutInitWindowSize(1024, 512) glutCreateWindow(b"Networked Raycaster Client") +glEnable(GL_BLEND) +glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glutReshapeFunc(renderer.reshape) diff --git a/raycaster/Renderer.py b/raycaster/Renderer.py index 5bbbef5..6c1df38 100644 --- a/raycaster/Renderer.py +++ b/raycaster/Renderer.py @@ -6,12 +6,13 @@ from OpenGL.GLUT import * class Renderer: - def __init__(self, map_obj, fov:int = 80, num_rays = 120, max_distance = 500, minimap_size = 4): + 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.""" @@ -28,12 +29,12 @@ def get_window_size(self): height = glutGet(GLUT_WINDOW_HEIGHT) return width, height - def draw_scene(self, player): + def draw_scene(self, player:Player): self.cast_rays(player) self.draw_minimap() self.draw_minimap_player(player) - def cast_rays(self, player): + def cast_rays(self, player:Player): screen_width, screen_height = self.get_window_size() slice_width = screen_width / self.num_rays @@ -146,14 +147,14 @@ def cast_rays(self, player): 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 - glColor3ub(*self.map.get_color(str(cell_value))) + glColor4ub(*self.map.get_color(str(cell_value)),minimap_opacity) else: - glColor3ub(*self.map.get_color(str("ground"))) + glColor4ub(*self.map.get_color(str("ground")),minimap_opacity) xo, yo = x * minimap_size, y * minimap_size glBegin(GL_QUADS) @@ -167,13 +168,14 @@ def draw_minimap_player(self, player_or_coords: Union[Player, Tuple[float, float mapS = self.get_map().get_mapS() minimap_size = self.get_minimap_size() - segment_length = 0.25 + minimap_opacity = self.get_minimap_opacity() + segment_length = 10 if isinstance(player_or_coords, Player): - glColor3f(0, 1, 1) # CYAN COLOR FOR 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: - glColor3f(1, 0, 0) # RED COLOR FOR OTHER PLAYERS + 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)) @@ -195,7 +197,7 @@ def draw_minimap_player(self, player_or_coords: Union[Player, Tuple[float, float # Forward segment glBegin(GL_LINES) glVertex2i(int(map_x), int(map_y)) - glVertex2i(int(map_x + 10 * pdx), int(map_y + 10 * pdy)) + glVertex2i(int(map_x + segment_length * pdx), int(map_y + segment_length * pdy)) glEnd() # Getters @@ -215,5 +217,5 @@ def get_max_distance(self): def get_minimap_size(self): return self.minimap_size - - + def get_minimap_opacity(self): + return self.minimap_opacity \ No newline at end of file From 20c856ce83d443b4af8d32e0a02097c875377639 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Sat, 22 Mar 2025 18:37:21 +0100 Subject: [PATCH 38/45] feat(logging): add logging system --- .env | 7 +++- .gitignore | 3 +- logger/CustomFormatter.py | 64 ++++++++++++++++++++++++++++++++++++ logger/__init__.py | 1 + main.py | 12 +++++-- multiplayer/ClientNetwork.py | 15 +++++---- raycaster/Map.py | 11 ++++--- server_network.py | 46 +++++++++++++++----------- 8 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 logger/CustomFormatter.py create mode 100644 logger/__init__.py diff --git a/.env b/.env index db55431..164b07d 100644 --- a/.env +++ b/.env @@ -15,4 +15,9 @@ MAP_MINIMAP_OPACITY=210 # range from 0 to 255 KEY_FORWARD=z KEY_LEFT=q KEY_BACKWARD=s -KEY_RIGHT=d \ No newline at end of file +KEY_RIGHT=d + +# === DEV OPTIONS === + +# LOGGER +LOG_LEVEL=INFO \ No newline at end of file diff --git a/.gitignore b/.gitignore index 37bbdde..3b201a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ -temp/ \ No newline at end of file +temp/ +*.log \ No newline at end of file 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 index f6e935f..87b4349 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ # 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 * @@ -18,8 +18,14 @@ # 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)) @@ -50,7 +56,7 @@ def get_env_str(key, default): # === GAME INITIALIZATION === # Wait for map data while network.map_data is None: - print("[CLIENT] Waiting for map...") + log.info("Waiting for 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"]) @@ -124,7 +130,7 @@ def display(): glutSwapBuffers() - # print(f"[CLIENT] Rendering players_state: {network.players_state}") # DEBUG + log.debug(f"Rendering players_state: {network.players_state}") # === GLUT INITIALIZATION PART 2 === diff --git a/multiplayer/ClientNetwork.py b/multiplayer/ClientNetwork.py index a5afda2..63c4fc8 100644 --- a/multiplayer/ClientNetwork.py +++ b/multiplayer/ClientNetwork.py @@ -2,6 +2,9 @@ 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): @@ -45,23 +48,23 @@ def listen(self): # Handle message if 'init_id' in message: self.my_id = message['init_id'] - print(f"[CLIENT] Assigned Player ID: {self.my_id}") + log.info(f"Assigned Player ID: {self.my_id}") # Send ACK ack = json.dumps({"ack": True}).encode() length = struct.pack('!I', len(ack)) self.server.sendall(length + ack) - print("[CLIENT] Sent ACK to server") + log.info("Sent ACK to server") elif 'map_data' in message: self.map_data = message['map_data'] - print(f"[CLIENT] Received map data: {self.map_data}") + log.info(f"Received map data: {self.map_data}") else: self.players_state = message - # print(f"[CLIENT] Updated players_state: {self.players_state}") # DEBUG + log.debug(f"Updated players_state: {self.players_state}") except Exception as e: - print(f"[CLIENT] Listen error: {e}") + log.critical(f"Listen error: {e}") self.running = False def send_player_update(self, px, py, pa): @@ -71,7 +74,7 @@ def send_player_update(self, px, py, pa): length = struct.pack('!I', len(message_bytes)) self.server.sendall(length + message_bytes) except Exception as e: - print(f"[CLIENT] Send error: {e}") + log.error(f"Send error: {e}") def close(self): self.running = False diff --git a/raycaster/Map.py b/raycaster/Map.py index 1a7889a..ef8696e 100644 --- a/raycaster/Map.py +++ b/raycaster/Map.py @@ -1,5 +1,8 @@ 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): @@ -27,13 +30,13 @@ def save_to_file(self, filename): } with open(filename, 'w') as f: json.dump(map_data, f) - print(f"[MAP] Saved map to {filename}") + 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) - print(f"[MAP] Loaded map from {filename}") + 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): @@ -51,8 +54,8 @@ def get_color(self, texture): try: color = self.colorMap[texture] except: - color = (1,0,1) # MAGENTA FOR ERROR - print(f"[MAP] TEXTURE {texture} DOES NOT EXIST") + color = (255,0,255) # MAGENTA FOR ERROR + log.error(f"TEXTURE {texture} DOES NOT EXIST") return color def get_spawnpoint(self): diff --git a/server_network.py b/server_network.py index 37eb7d0..6174757 100644 --- a/server_network.py +++ b/server_network.py @@ -4,8 +4,11 @@ import struct import sys import os +from logger import get_logger from raycaster import Map +log = get_logger(__name__) + clients = {} # conn: player_id players_state = {} # player_id: {px, py, pa} @@ -26,46 +29,46 @@ def recv_exact(sock, n): return data def broadcast(): - print(f"[SERVER] Broadcasting: {players_state}") # DEBUG + log.debug(f"Broadcasting: {players_state}") for conn in list(clients.keys()): try: send_message(conn, players_state) except Exception as e: - print(f"[SERVER] Broadcast error: {e}") + log.error(f"Broadcast error: {e}") conn.close() clients.pop(conn, None) def handle_client(conn, addr, player_id, map_data): - print(f"[SERVER] New connection from {addr}") + log.info(f"New connection from {addr}") clients[conn] = player_id # Step 1: Send init_id send_message(conn, {"init_id": player_id}) - print(f"[SERVER] Sent init_id to {addr}") + log.info(f"Sent init_id to {addr}") # Send map data send_message(conn, {"map_data": map_data}) - print(f"[SERVER] Sent map_data to {addr}") + log.info(f"Sent map_data to {addr}") # Step 2: Wait for ACK before broadcasting raw_len = recv_exact(conn, 4) if not raw_len: - print(f"[SERVER] No ACK received from {addr}") + log.info(f"No ACK received from {addr}") return msg_len = struct.unpack('!I', raw_len)[0] data = recv_exact(conn, msg_len) - #[ ] FIXME: make sure to only broadcast to those with ack recieved / close connexion when invalid ack. - # Otherwise you can join from browser, close the page, connexion is never closed and it keeps broadcasting to you. + #[ ] FIXME: make sure to only broadcast to those with ack recieved / close connection when invalid ack. + # Otherwise you can join from browser, close the page, connection is never closed and it keeps broadcasting to you. # Check with chatgpt if this might cause issues with normal clients try: ack_msg = json.loads(data.decode()) except: ack_msg = {} - print(f"[SERVER] No ACK from {addr}") + log.warning(f"No ACK from {addr}") if 'ack' not in ack_msg: - print(f"[SERVER] Invalid ACK from {addr}") + log.warning(f"Invalid ACK from {addr}") return - print(f"[SERVER] Received ACK from {addr}") + log.info(f"Received ACK from {addr}") # Now safe to broadcast broadcast() @@ -82,9 +85,9 @@ def handle_client(conn, addr, player_id, map_data): broadcast() except Exception as e: - print(f"[SERVER] Error: {e}") + log.warning(f"{e}") finally: - print(f"[SERVER] Connection lost: {addr}") + log.info(f"Connection lost: {addr}") conn.close() if conn in clients: clients.pop(conn) @@ -94,6 +97,7 @@ def handle_client(conn, addr, player_id, map_data): def choose_map(): + log.info("Choosing map...") MAPS_DIRECTORY = 'maps/' # List all files in the maps directory @@ -121,31 +125,33 @@ def choose_map(): print("Invalid input. Please enter a number.") # Load and return map - print(f"Loading map: {choosen_map}") + log.info(f"Loading map: {choosen_map}") return Map.load_from_file(os.path.join(MAPS_DIRECTORY, choosen_map)).map_to_dict() def start_server(host='127.0.0.1', port=5555): + + log.info("Starting server...") # Map map_data = choose_map() + log.info("Binding server to port...") + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((host, port)) server.listen() - print(f"[SERVER] Listening on {host}:{port}") + + log.info(f"Listening on {host}:{port}") next_player_id = 1 - + log.info("Starting accepting connection loop") while True: conn, addr = server.accept() player_id = next_player_id next_player_id += 1 - - # Initialize player's state - players_state[player_id] = {"px": map_data["spawnpoint"][0], "py": map_data["spawnpoint"][1], "pa": 90} - + # Start handler thread = threading.Thread(target=handle_client, args=(conn, addr, player_id, map_data), daemon=True) thread.start() From 10cd8fc71a65dfa6766e674fef85ad63068a48d1 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Sun, 23 Mar 2025 15:31:17 +0100 Subject: [PATCH 39/45] feat(network): add server shutdown sig detection --- main.py | 8 +- multiplayer/ClientNetwork.py | 37 ++++++--- server_network.py | 157 +++++++++++++++++++++-------------- 3 files changed, 127 insertions(+), 75 deletions(-) diff --git a/main.py b/main.py index 87b4349..d3adef0 100644 --- a/main.py +++ b/main.py @@ -55,8 +55,10 @@ def get_env_str(key, default): # === GAME INITIALIZATION === # Wait for map data +log.info("Waiting for map...") while network.map_data is None: - log.info("Waiting for map...") + 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"]) @@ -102,6 +104,10 @@ def keyboard_up(key, x, y): # === 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 diff --git a/multiplayer/ClientNetwork.py b/multiplayer/ClientNetwork.py index 63c4fc8..1c5be05 100644 --- a/multiplayer/ClientNetwork.py +++ b/multiplayer/ClientNetwork.py @@ -32,49 +32,64 @@ def recv_exact(self, sock, n): def listen(self): try: while self.running: - # Step 1: Read length raw_len = self.recv_exact(self.server, 4) if not raw_len: - break - msg_len = struct.unpack('!I', raw_len)[0] + log.warning("Server disconnected (length header missing). Closing client.") + break # Stop loop - # Step 2: Read full message + 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 message + # Handle messages: if 'init_id' in message: self.my_id = message['init_id'] log.info(f"Assigned Player ID: {self.my_id}") # Send ACK - ack = json.dumps({"ack": True}).encode() - length = struct.pack('!I', len(ack)) - self.server.sendall(length + ack) - log.info("Sent ACK to server") + 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: {self.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.error(f"Send error: {e}") + log.critical(f"Send failed (server down?): {e}") + self.running = False + def close(self): self.running = False diff --git a/server_network.py b/server_network.py index 6174757..98cabb1 100644 --- a/server_network.py +++ b/server_network.py @@ -4,6 +4,7 @@ import struct import sys import os +import signal from logger import get_logger from raycaster import Map @@ -11,6 +12,8 @@ clients = {} # conn: player_id players_state = {} # player_id: {px, py, pa} +shutdown_event = threading.Event() + # === Helper functions === @@ -19,6 +22,7 @@ def send_message(conn, message_dict): length = struct.pack('!I', len(message)) conn.sendall(length + message) + def recv_exact(sock, n): data = b'' while len(data) < n: @@ -28,6 +32,7 @@ def recv_exact(sock, n): data += packet return data + def broadcast(): log.debug(f"Broadcasting: {players_state}") for conn in list(clients.keys()): @@ -38,43 +43,41 @@ def broadcast(): 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 - # Step 1: 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}") - - # Step 2: 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) - #[ ] FIXME: make sure to only broadcast to those with ack recieved / close connection when invalid ack. - # Otherwise you can join from browser, close the page, connection is never closed and it keeps broadcasting to you. - # Check with chatgpt if this might cause issues with normal clients - try: - ack_msg = json.loads(data.decode()) - except: - ack_msg = {} - log.warning(f"No ACK from {addr}") - if 'ack' not in ack_msg: - log.warning(f"Invalid ACK from {addr}") - return - log.info(f"Received ACK from {addr}") - - # Now safe to broadcast - broadcast() - try: - while True: + # 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 @@ -84,15 +87,16 @@ def handle_client(conn, addr, player_id, map_data): players_state[player_id] = message broadcast() + + except ConnectionAbortedError: + log.warning(f"Connection aborted: {addr}") except Exception as e: - log.warning(f"{e}") + log.warning(f"Client error: {e}") finally: log.info(f"Connection lost: {addr}") conn.close() - if conn in clients: - clients.pop(conn) - if player_id in players_state: - players_state.pop(player_id) + clients.pop(conn, None) + players_state.pop(player_id, None) broadcast() @@ -100,61 +104,88 @@ def choose_map(): log.info("Choosing map...") MAPS_DIRECTORY = 'maps/' - # List all files in the maps directory + # 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 in the directory!") - return None + print("No map files found!") + sys.exit(1) - # Display files print("Available maps:") for idx, filename in enumerate(files): print(f"{idx + 1}. {filename}") - # Ask user to choose while True: try: choice = int(input("Choose a map by number: ")) - 1 if 0 <= choice < len(files): - choosen_map = files[choice] + chosen_map = files[choice] break else: - print("Invalid choice. Please choose a valid number.") + print("Invalid choice.") except ValueError: - print("Invalid input. Please enter a number.") + print("Enter a valid number.") - # Load and return map - log.info(f"Loading map: {choosen_map}") - return Map.load_from_file(os.path.join(MAPS_DIRECTORY, choosen_map)).map_to_dict() + 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 map_data = choose_map() - log.info("Binding server to port...") - 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 - - log.info("Starting accepting connection loop") - while True: - conn, addr = server.accept() - player_id = next_player_id - next_player_id += 1 - - # Start handler - thread = threading.Thread(target=handle_client, args=(conn, addr, player_id, map_data), daemon=True) - thread.start() + 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() From 3e7876fc3fc5c82c3a13a2689044c62796c83ddf Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Sun, 23 Mar 2025 16:20:28 +0100 Subject: [PATCH 40/45] update maps --- maps/aryantech123.json | 98 +- maps/empty_l.json | 290 ++++- maps/empty_xl.json | 2538 ++++++++++++++++++++++++++++++++++++++++ maps/house_l.json | 295 ++++- maps/map.json | 103 +- maps/test.json | 11 +- 6 files changed, 3330 insertions(+), 5 deletions(-) create mode 100644 maps/empty_xl.json diff --git a/maps/aryantech123.json b/maps/aryantech123.json index 5bcf942..b2408df 100644 --- a/maps/aryantech123.json +++ b/maps/aryantech123.json @@ -1 +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], "wall": [219,73,0]}} \ No newline at end of file +{ + "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 index 68ff7ce..e56d4b7 100644 --- a/maps/empty_l.json +++ b/maps/empty_l.json @@ -1 +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], "wall": [219,73,0]}} \ No newline at end of file +{ + "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 index c877725..1f0fa6d 100644 --- a/maps/house_l.json +++ b/maps/house_l.json @@ -1 +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], "wall": [219,73,0]}} \ No newline at end of file +{ + "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/map.json b/maps/map.json index 5bcf942..a96e377 100644 --- a/maps/map.json +++ b/maps/map.json @@ -1 +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], "wall": [219,73,0]}} \ No newline at end of file +{ + "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/test.json b/maps/test.json index 37e04d7..f5d8d29 100644 --- a/maps/test.json +++ b/maps/test.json @@ -216,6 +216,15 @@ 228, 92, 96 + ], + "2": [ + 104, + 62, + 23 ] - } + }, + "spawnpoint": [ + 768.0, + 64.0 + ] } \ No newline at end of file From 3f5b641e114b9e4170c396855e81b44513c0d15c Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Sun, 23 Mar 2025 16:55:25 +0100 Subject: [PATCH 41/45] fix(network): connectionResetError not recognised --- server_network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server_network.py b/server_network.py index 98cabb1..bf1de76 100644 --- a/server_network.py +++ b/server_network.py @@ -89,7 +89,9 @@ def handle_client(conn, addr, player_id, map_data): broadcast() except ConnectionAbortedError: - log.warning(f"Connection aborted: {addr}") + 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: From 17ed899238b98b5f7bd0bf9a1fc09aa3b46b4bcb Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Sun, 23 Mar 2025 16:56:31 +0100 Subject: [PATCH 42/45] update TODO --- TODO | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index e7862f6..fdd2ed0 100644 --- a/TODO +++ b/TODO @@ -1 +1,2 @@ -[ ] Use texture mapping \ No newline at end of file +[ ] Use texture mapping +[ ] Multiplayer: render other players \ No newline at end of file From a351dfbaf1113109d9363d23f217bfceadc6026e Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Sun, 23 Mar 2025 17:16:34 +0100 Subject: [PATCH 43/45] chore: rename .env to .env.example --- .env => .env.example | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .env => .env.example (100%) diff --git a/.env b/.env.example similarity index 100% rename from .env rename to .env.example From 38f39bd269c76bf48f62bbd16bd92253914ee615 Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 25 Mar 2025 17:39:48 +0100 Subject: [PATCH 44/45] update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3b201a4..b02c49a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ __pycache__ temp/ -*.log \ No newline at end of file +*.log +env +.env \ No newline at end of file From 3dd885e888881b4d054f161bdfa74bd986c9090f Mon Sep 17 00:00:00 2001 From: MxPerrot Date: Tue, 25 Mar 2025 17:40:05 +0100 Subject: [PATCH 45/45] remove useless import --- main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index d3adef0..f708b42 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ ############################################################################### # IMPORTS # ############################################################################### -from alive_progress import alive_bar +#from alive_progress import alive_bar from logger import get_logger from raycaster import Map, Player, Renderer from OpenGL.GL import * @@ -38,10 +38,10 @@ def get_env_str(key, 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') +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)