From ed582c3f3917fe3fd3794831223e2fe7f0dba728 Mon Sep 17 00:00:00 2001 From: colas-sebastien Date: Mon, 4 Aug 2025 20:14:45 +0200 Subject: [PATCH 1/3] dmd snake added to the project --- dmd_simulator/cli/games/dmd_snake.py | 316 +++++++++++++++++++++++++ dmd_simulator/data/snake/dmd-snake.png | Bin 0 -> 1645 bytes dmd_simulator/data/snake/snake.xcf | Bin 0 -> 7556 bytes pyproject.toml | 1 + 4 files changed, 317 insertions(+) create mode 100755 dmd_simulator/cli/games/dmd_snake.py create mode 100644 dmd_simulator/data/snake/dmd-snake.png create mode 100644 dmd_simulator/data/snake/snake.xcf diff --git a/dmd_simulator/cli/games/dmd_snake.py b/dmd_simulator/cli/games/dmd_snake.py new file mode 100755 index 0000000..127a42c --- /dev/null +++ b/dmd_simulator/cli/games/dmd_snake.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 + +# dmd-snake +# +# Based on the code +# https://github.com/GeorgeZhukov/python-snake/blob/master/snake.py + +import math +import time +import sys +from random import randint +from PIL import Image, ImageDraw +from dmd_simulator.dmd_player import DmdPlayer +from dmd_simulator.dmd_font import DmdFont +from importlib import resources as impresources +import time +import sdl2 + +colors = [ + ( 0, 0, 0), + ( 0, 255, 0), + (255, 0, 0), + (255, 255, 0), + (255, 255, 0), + ( 0, 255, 255), + (255, 0, 255), +] + +class DMDConf: + port=6789 + host="localhost" + width=128 + height=32 + bock_size=4 + x_shift=1 + y_shift=23 + width_blocks=math.floor(width/bock_size) + height_blocks=math.floor(height/bock_size) + +class Field: + def __init__(self, width, height): + self.width = width + self.height = height + self.snake_coords = [] + self._generate_field() + self.add_entity() + + def add_entity(self): + while(True): + x = randint(0, self.width-1) + y = randint(0, self.height-1) + entity = [x, y] + if entity not in self.snake_coords: + self.field[x][y] = 3 + break + + def _generate_field(self): + self.field = [[0 for y in range(self.height)] for x in range(self.width)] + + def _clear_field(self): + self.field = [[j if j!= 1 and j!= 2 else 0 for j in i] for i in self.field] + + def render(self): + self._clear_field() + + # Render snake on the field + for x, y in self.snake_coords: + self.field[x][y] = 1 + + # Mark head + head = self.snake_coords[-1] + self.field[head[0]][head[1]] = 2 + + def get_entity_pos(self): + for y in range(self.height): + for x in range(self.width): + if self.field[x][y] == 3: + return [x, y] + + return [-1, -1] + + + def is_snake_eat_entity(self): + entity = self.get_entity_pos() + head = self.snake_coords[-1] + return entity == head + +class Snake: + def __init__(self, width, height,speed): + self.height = height + self.width = width + self.direction = 0 + self.speed=speed + self.alive=True + # directions: + # - 0: right + # - 1: up + # - 2: left + # - 3: down + + # Init basic coords + self.coords = [[0, 0], [0, 1], [0, 2], [0, 3]] + + def set_direction(self, dir): + self.direction = (self.direction + dir)%4 + + def level_up(self): + # get last point direction + a = self.coords[0] + b = self.coords[1] + + tail = a[:] + + if a[0] < b[0]: + tail[0]-=1 + elif a[1] < b[1]: + tail[1]-=1 + elif a[0] > b[0]: + tail[0]+=1 + elif a[1] > b[1]: + tail[1]+=1 + + tail = self._check_limit(tail) + self.coords.insert(0, tail) + + self.speed=self.speed*0.95 + + def is_alive(self): + head = self.coords[-1] + snake_body = self.coords[:-1] + return head not in snake_body + + def _check_limit(self, point): + # Check field limit + if point[0] > self.width-1: + point[0] = 0 + elif point[0] < 0: + point[0] = self.width -1 + elif point[1] < 0: + point[1] = self.height -1 + elif point[1] > self.height-1: + point[1] = 0 + return point + + def move(self): + # Determine head coords + head = self.coords[-1][:] + + # Calc new head coords + if self.direction == 1: + head[1]-=1 + elif self.direction == 3: + head[1]+=1 + elif self.direction == 0: + head[0]+=1 + elif self.direction == 2: + head[0]-=1 + + # Check field limit + head = self._check_limit(head) + + del(self.coords[0]) + self.coords.append(head) + self.field.snake_coords = self.coords + + self.alive=self.is_alive() + + # check if snake eat an entity + if self.field.is_snake_eat_entity(): + self.level_up() + self.field.add_entity() + + def set_field(self, field): + self.field = field + + def args(): + return DMDConf + + +def dmd_snake_launch() -> None: + conf=Snake.args() + client=DmdPlayer.connect(conf) + width = conf.width + height = conf.height + layer = "main" + header = DmdPlayer.getHeader(width, height, layer, width * height * 2) # RGB565 + dmdresroot = impresources.files("dmd_simulator") + dmdrespath = dmdresroot.joinpath("data/snake") + + dmdfont=DmdFont("minogram_6x10") + + # Define some colors + WHITE = (255, 255, 255) + + # Init snake & field + field = Field(conf.width_blocks, conf.height_blocks) + snake = Snake(conf.width_blocks, conf.height_blocks,0.3) + snake.set_field(field) + + joysticks = {} + + DmdPlayer.sendImageFile(header,client,0,dmdrespath.joinpath("dmd-snake.png"),128,32,True) + + sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER) + + time.sleep(2) + + pressing_left=False + pressing_right=False + while snake.alive: + event = sdl2.SDL_Event() + while sdl2.SDL_PollEvent(event) != 0: + if event.type == sdl2.SDL_QUIT: + done = True + # If some arrows did pressed - change direction + # snake.set_direction(ch) + if (event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT) and (not pressing_left): + snake.set_direction(+1) + pressing_left=True + if (event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT) and (not pressing_right): + snake.set_direction(-1) + pressing_right=True + + if event.type == sdl2.SDL_CONTROLLERBUTTONUP: + if event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT: + pressing_left=False + if event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + pressing_right=False + + if event.type == sdl2.SDL_CONTROLLERDEVICEADDED: + joy = sdl2.SDL_GameControllerOpen(event.cdevice.which) + if joy: + joysticks[event.cdevice.which] = joy + + if event.type == sdl2.SDL_CONTROLLERDEVICEREMOVED: + del joysticks[event.cdevice.which] + + if event.type == sdl2.SDL_JOYHATMOTION: + if sdl2.SDL_IsGameController(event.jdevice.which) == False: + if event.jhat.hat == 0: + if event.jhat.value == 8: # left + snake.set_direction(+1) + pressing_left=True + pressing_right=False + elif event.jhat.value == 2: # right + snake.set_direction(-1) + pressing_right=True + pressing_left=False + elif event.jhat.value == 1 or event.jhat.value == 0: + pressing_left=False + pressing_right=False + + if event.type == sdl2.SDL_JOYDEVICEADDED: + if sdl2.SDL_IsGameController(event.jdevice.which) == False: + joy = sdl2.SDL_JoystickOpen(event.jdevice.which) + if joy: + joysticks[event.jdevice.which] = joy + + if event.type == sdl2.SDL_JOYDEVICEREMOVED: + if sdl2.SDL_IsGameController(event.jdevice.which) == False: + del joysticks[event.jdevice.which] + + # Move snake + snake.move() + + # Render field + field.render() + + im = Image.new("RGBA", (width, height)) + txt="Score" + dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),2,WHITE) + txt=str(len(field.snake_coords)-4) + dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),12,WHITE) + + for y in range(conf.height_blocks): + for x in range(conf.width_blocks): + if (field.field[x][y]!=0): + for a in range(conf.bock_size): + for b in range(conf.bock_size): + im.putpixel((x*conf.bock_size+a, y*conf.bock_size+b), colors[field.field[x][y]]) + if (field.field[x][y]==2): + if (snake.direction==0): + im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size), colors[0]) + im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size+conf.bock_size-1), colors[0]) + elif (snake.direction==1): + im.putpixel((x*conf.bock_size, y*conf.bock_size), colors[0]) + im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size), colors[0]) + elif (snake.direction==2): + im.putpixel((x*conf.bock_size, y*conf.bock_size), colors[0]) + im.putpixel((x*conf.bock_size, y*conf.bock_size+conf.bock_size-1), colors[0]) + elif (snake.direction==3): + im.putpixel((x*conf.bock_size, y*conf.bock_size+conf.bock_size-1), colors[0]) + im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size+conf.bock_size-1), colors[0]) + DmdPlayer.sendFrame(header, client, layer, DmdPlayer.imageConvert(im, True)) + + time.sleep(snake.speed) + + txt="Score" + dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),2,WHITE) + txt=str(len(field.snake_coords)-4) + dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),12,WHITE) + txt="Game Over" + dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),20,WHITE) + + + DmdPlayer.sendFrame(header, client, layer, DmdPlayer.imageConvert(im, True)) + + client.close() + for joystick in joysticks: + if sdl2.SDL_IsGameController(joystick): + sdl2.SDL_GameControllerClose(joysticks[joystick]) + else: + sdl2.SDL_JoystickClose(joysticks[joystick]) + +if __name__ == '__main__': + dmd_snake_launch() diff --git a/dmd_simulator/data/snake/dmd-snake.png b/dmd_simulator/data/snake/dmd-snake.png new file mode 100644 index 0000000000000000000000000000000000000000..2b43beeaec41dcdabba35df815f53f2dfa041fc8 GIT binary patch literal 1645 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 91e18c1..70b302b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dmd-play = "dmd_simulator.cli.dmd_play:dmd_play_launch" dmd-simulator = "dmd_simulator.cli.dmd_simulator:dmd_simulator_launch" # games dmd-tetris = "dmd_simulator.cli.games.dmd_tetris:dmd_tetris_launch" +dmd-snake = "dmd_simulator.cli.games.dmd_snake:dmd_snake_launch" [tool.hatch.version] source = "code" From 13f697ec270349af2ccc979478a5c28b80a0f9b7 Mon Sep 17 00:00:00 2001 From: colas-sebastien Date: Thu, 28 Aug 2025 11:02:31 +0200 Subject: [PATCH 2/3] xcf delete --- dmd_simulator/data/snake/snake.xcf | Bin 7556 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dmd_simulator/data/snake/snake.xcf diff --git a/dmd_simulator/data/snake/snake.xcf b/dmd_simulator/data/snake/snake.xcf deleted file mode 100644 index 9f12115b1021454b0ce019c1cc2c2396da70d6b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7556 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 From 7f57019b62de57caa9b18c1e5c8f2364b6a49204 Mon Sep 17 00:00:00 2001 From: colas-sebastien Date: Fri, 26 Sep 2025 10:59:09 +0200 Subject: [PATCH 3/3] Code more modular + remote control for PPUC project --- dmd_simulator/cli/games/dmd_snake.py | 198 ++++++++------------------ dmd_simulator/cli/games/dmd_tetris.py | 59 ++++---- dmd_simulator/cli/games/ws_client.py | 19 +++ dmd_simulator/dmd_game.py | 195 +++++++++++++++++++++++++ 4 files changed, 307 insertions(+), 164 deletions(-) create mode 100755 dmd_simulator/cli/games/ws_client.py create mode 100644 dmd_simulator/dmd_game.py diff --git a/dmd_simulator/cli/games/dmd_snake.py b/dmd_simulator/cli/games/dmd_snake.py index 127a42c..6cee2d3 100755 --- a/dmd_simulator/cli/games/dmd_snake.py +++ b/dmd_simulator/cli/games/dmd_snake.py @@ -12,7 +12,7 @@ from PIL import Image, ImageDraw from dmd_simulator.dmd_player import DmdPlayer from dmd_simulator.dmd_font import DmdFont -from importlib import resources as impresources +from dmd_simulator.dmd_game import DmdGame import time import sdl2 @@ -26,16 +26,9 @@ (255, 0, 255), ] -class DMDConf: - port=6789 - host="localhost" - width=128 - height=32 - bock_size=4 - x_shift=1 - y_shift=23 - width_blocks=math.floor(width/bock_size) - height_blocks=math.floor(height/bock_size) +WHITE = (255, 255, 255) + + class Field: def __init__(self, width, height): @@ -76,22 +69,21 @@ def get_entity_pos(self): for x in range(self.width): if self.field[x][y] == 3: return [x, y] - return [-1, -1] - def is_snake_eat_entity(self): entity = self.get_entity_pos() head = self.snake_coords[-1] return entity == head -class Snake: - def __init__(self, width, height,speed): - self.height = height - self.width = width - self.direction = 0 - self.speed=speed +class Snake(DmdGame): + def __init__(self,game_name,font_ld,font_hd): + super().__init__(game_name,font_ld,font_hd) + + self.width_blocks=math.floor(self.width/self.block_size) + self.height_blocks=math.floor(self.height/self.block_size) self.alive=True + self.direction = 0 # directions: # - 0: right # - 1: up @@ -123,7 +115,7 @@ def level_up(self): tail = self._check_limit(tail) self.coords.insert(0, tail) - self.speed=self.speed*0.95 + self.speed=self.speed*self.acceleration def is_alive(self): head = self.coords[-1] @@ -132,13 +124,13 @@ def is_alive(self): def _check_limit(self, point): # Check field limit - if point[0] > self.width-1: + if point[0] > self.width_blocks-1: point[0] = 0 elif point[0] < 0: - point[0] = self.width -1 + point[0] = self.width_blocks -1 elif point[1] < 0: - point[1] = self.height -1 - elif point[1] > self.height-1: + point[1] = self.height_blocks -1 + elif point[1] > self.height_blocks-1: point[1] = 0 return point @@ -173,144 +165,74 @@ def move(self): def set_field(self, field): self.field = field - def args(): - return DMDConf - + def args(self,parser): + super().args(parser) + parser.add_argument("--block_size", type=int, default=4, help="game block size in pixels (default 4)") + parser.add_argument("--speed", type=float, default=0.3, help="Initial speed in seconds (default 0.3)") + parser.add_argument("--acceleration", type=float, default=0.95, help="Acceleration speed=speed*acceleration (default 0.95)") -def dmd_snake_launch() -> None: - conf=Snake.args() - client=DmdPlayer.connect(conf) - width = conf.width - height = conf.height - layer = "main" - header = DmdPlayer.getHeader(width, height, layer, width * height * 2) # RGB565 - dmdresroot = impresources.files("dmd_simulator") - dmdrespath = dmdresroot.joinpath("data/snake") + def drawScore(self): + txt="Score" + self.font.puttext(self.frame,txt,0,round((self.width-(len(txt)-1)*6)/2),2,WHITE) + txt=str(len(self.field.snake_coords)-4) + self.font.puttext(self.frame,txt,0,round((self.width-(len(txt)-1)*6)/2),12,WHITE) - dmdfont=DmdFont("minogram_6x10") + def drawGameOver(self): + txt="Game Over" + self.font.puttext(self.frame,txt,0,round((self.width-(len(txt)-1)*6)/2),20,WHITE) + - # Define some colors - WHITE = (255, 255, 255) +def dmd_snake_launch() -> None: + snake=Snake("snake","minogram_6x10","minogram_6x10") - # Init snake & field - field = Field(conf.width_blocks, conf.height_blocks) - snake = Snake(conf.width_blocks, conf.height_blocks,0.3) + # Init field + field = Field(snake.width_blocks, snake.height_blocks) snake.set_field(field) - - joysticks = {} - - DmdPlayer.sendImageFile(header,client,0,dmdrespath.joinpath("dmd-snake.png"),128,32,True) - - sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER) - - time.sleep(2) - pressing_left=False - pressing_right=False while snake.alive: - event = sdl2.SDL_Event() - while sdl2.SDL_PollEvent(event) != 0: - if event.type == sdl2.SDL_QUIT: - done = True - # If some arrows did pressed - change direction - # snake.set_direction(ch) - if (event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT) and (not pressing_left): - snake.set_direction(+1) - pressing_left=True - if (event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT) and (not pressing_right): - snake.set_direction(-1) - pressing_right=True - - if event.type == sdl2.SDL_CONTROLLERBUTTONUP: - if event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT: - pressing_left=False - if event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT: - pressing_right=False - - if event.type == sdl2.SDL_CONTROLLERDEVICEADDED: - joy = sdl2.SDL_GameControllerOpen(event.cdevice.which) - if joy: - joysticks[event.cdevice.which] = joy - - if event.type == sdl2.SDL_CONTROLLERDEVICEREMOVED: - del joysticks[event.cdevice.which] - - if event.type == sdl2.SDL_JOYHATMOTION: - if sdl2.SDL_IsGameController(event.jdevice.which) == False: - if event.jhat.hat == 0: - if event.jhat.value == 8: # left - snake.set_direction(+1) - pressing_left=True - pressing_right=False - elif event.jhat.value == 2: # right - snake.set_direction(-1) - pressing_right=True - pressing_left=False - elif event.jhat.value == 1 or event.jhat.value == 0: - pressing_left=False - pressing_right=False - - if event.type == sdl2.SDL_JOYDEVICEADDED: - if sdl2.SDL_IsGameController(event.jdevice.which) == False: - joy = sdl2.SDL_JoystickOpen(event.jdevice.which) - if joy: - joysticks[event.jdevice.which] = joy - - if event.type == sdl2.SDL_JOYDEVICEREMOVED: - if sdl2.SDL_IsGameController(event.jdevice.which) == False: - del joysticks[event.jdevice.which] + snake.controller.pollEvent() + + if (snake.controller.LeftWasPressed()): + snake.set_direction(+1) + + if (snake.controller.RightWasPressed()): + snake.set_direction(-1) # Move snake snake.move() # Render field field.render() + snake.newFrame() - im = Image.new("RGBA", (width, height)) - txt="Score" - dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),2,WHITE) - txt=str(len(field.snake_coords)-4) - dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),12,WHITE) + im=snake.frame - for y in range(conf.height_blocks): - for x in range(conf.width_blocks): + for y in range(snake.height_blocks): + for x in range(snake.width_blocks): if (field.field[x][y]!=0): - for a in range(conf.bock_size): - for b in range(conf.bock_size): - im.putpixel((x*conf.bock_size+a, y*conf.bock_size+b), colors[field.field[x][y]]) + for a in range(snake.block_size): + for b in range(snake.block_size): + im.putpixel((x*snake.block_size+a, y*snake.block_size+b), colors[field.field[x][y]]) if (field.field[x][y]==2): if (snake.direction==0): - im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size), colors[0]) - im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size+conf.bock_size-1), colors[0]) + im.putpixel((x*snake.block_size+snake.block_size-1, y*snake.block_size), colors[0]) + im.putpixel((x*snake.block_size+snake.block_size-1, y*snake.block_size+snake.block_size-1), colors[0]) elif (snake.direction==1): - im.putpixel((x*conf.bock_size, y*conf.bock_size), colors[0]) - im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size), colors[0]) + im.putpixel((x*snake.block_size, y*snake.block_size), colors[0]) + im.putpixel((x*snake.block_size+snake.block_size-1, y*snake.block_size), colors[0]) elif (snake.direction==2): - im.putpixel((x*conf.bock_size, y*conf.bock_size), colors[0]) - im.putpixel((x*conf.bock_size, y*conf.bock_size+conf.bock_size-1), colors[0]) + im.putpixel((x*snake.block_size, y*snake.block_size), colors[0]) + im.putpixel((x*snake.block_size, y*snake.block_size+snake.block_size-1), colors[0]) elif (snake.direction==3): - im.putpixel((x*conf.bock_size, y*conf.bock_size+conf.bock_size-1), colors[0]) - im.putpixel((x*conf.bock_size+conf.bock_size-1, y*conf.bock_size+conf.bock_size-1), colors[0]) - DmdPlayer.sendFrame(header, client, layer, DmdPlayer.imageConvert(im, True)) - + im.putpixel((x*snake.block_size, y*snake.block_size+snake.block_size-1), colors[0]) + im.putpixel((x*snake.block_size+snake.block_size-1, y*snake.block_size+snake.block_size-1), colors[0]) + snake.displayScore() + snake.sendFrame() time.sleep(snake.speed) - txt="Score" - dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),2,WHITE) - txt=str(len(field.snake_coords)-4) - dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),12,WHITE) - txt="Game Over" - dmdfont.puttext(im,txt,0,round((width-(len(txt)-1)*6)/2),20,WHITE) - - - DmdPlayer.sendFrame(header, client, layer, DmdPlayer.imageConvert(im, True)) + snake.displayGameOver() + snake.end() - client.close() - for joystick in joysticks: - if sdl2.SDL_IsGameController(joystick): - sdl2.SDL_GameControllerClose(joysticks[joystick]) - else: - sdl2.SDL_JoystickClose(joysticks[joystick]) if __name__ == '__main__': dmd_snake_launch() diff --git a/dmd_simulator/cli/games/dmd_tetris.py b/dmd_simulator/cli/games/dmd_tetris.py index a799539..d1639bf 100755 --- a/dmd_simulator/cli/games/dmd_tetris.py +++ b/dmd_simulator/cli/games/dmd_tetris.py @@ -6,10 +6,13 @@ from PIL import Image, ImageDraw from dmd_simulator.dmd_player import DmdPlayer from dmd_simulator.dmd_font import DmdFont +from dmd_simulator.dmd_game import DmdGame import random from importlib import resources as impresources import time import sdl2 +import argparse + colors = [ ( 0, 0, 0), @@ -48,30 +51,17 @@ def image(self): def rotate(self): self.rotation = (self.rotation + 1) % len(self.figures[self.type]) -class DMDConf: - port=6789 - host="localhost" - width=128 - height=32 - bock_size=3 - x_shift=1 - y_shift=23 - height_blocks=34 - width_blocks=10 - -class Tetris: + + +class Tetris(DmdGame): def __init__(self, width, height): self.level = 1 self.score = 0 self.state = "start" self.field = [] - self.height = 0 - self.width = 0 self.x = 100 self.y = 60 - self.zoom = 20 - self.figure = None - + self.figure = None self.height = height self.width = width self.field = [] @@ -145,11 +135,28 @@ def rotate(self): if self.intersects(): self.figure.rotation = old_rotation - def args(): - return DMDConf + def args(self,parser): + super().args(self,parser) + parser.add_argument("--block_size", type=int, default= 3, help="game block size in pixels (default 3)") + + def conf(self,progName): + parser = argparse.ArgumentParser(prog=progName) + self.args(self,parser) + + conf=parser.parse_args() + if conf.hd: + conf.width = 256 + conf.height = 64 + + conf.x_shift=1 + conf.y_shift=23 + conf.height_blocks=34 + conf.width_blocks=10 + + return conf def dmd_tetris_launch() -> None: - conf=Tetris.args() + conf=Tetris.conf(Tetris,"dmd_tetris.py") client=DmdPlayer.connect(conf) width = conf.width height = conf.height @@ -279,18 +286,18 @@ def dmd_tetris_launch() -> None: for i in range(game.height): for j in range(game.width): if game.field[i][j] > 0: - for a in range(conf.bock_size): - for b in range(conf.bock_size): - im.putpixel((width - conf.bock_size - (i*conf.bock_size+a)-conf.y_shift, j*conf.bock_size+b+conf.x_shift), colors[game.field[i][j]]) + for a in range(conf.block_size): + for b in range(conf.block_size): + im.putpixel((width - conf.block_size - (i*conf.block_size+a)-conf.y_shift, j*conf.block_size+b+conf.x_shift), colors[game.field[i][j]]) if game.figure is not None: for i in range(4): for j in range(4): p = i * 4 + j if p in game.figure.image(): - for a in range(conf.bock_size): - for b in range(conf.bock_size): - im.putpixel((width - conf.bock_size - ((i + game.figure.y)*conf.bock_size+a)-conf.y_shift, (j + game.figure.x)*conf.bock_size+b+conf.x_shift), colors[game.figure.color]) + for a in range(conf.block_size): + for b in range(conf.block_size): + im.putpixel((width - conf.block_size - ((i + game.figure.y)*conf.block_size+a)-conf.y_shift, (j + game.figure.x)*conf.block_size+b+conf.x_shift), colors[game.figure.color]) dmdfont.puttext(im,"Score",1,116,1,WHITE) diff --git a/dmd_simulator/cli/games/ws_client.py b/dmd_simulator/cli/games/ws_client.py new file mode 100755 index 0000000..455b5d6 --- /dev/null +++ b/dmd_simulator/cli/games/ws_client.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +"""WebSocket Client to test remote control of the game""" + +from websockets.sync.client import connect +import argparse + +def message(args): + with connect("ws://"+args.wshost+":"+str(args.wsport)) as websocket: + websocket.send(args.action) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(prog="ws_client.py") + parser.add_argument("--wshost", default="0.0.0.0", help="WebSocket host (default 0.0.0.0)") + parser.add_argument("--wsport", type=int, default=8080, help="WebSocket port (default 8080)") + parser.add_argument("--action", default="right", help="action: left or right (default right)") + args=parser.parse_args() + message(args) \ No newline at end of file diff --git a/dmd_simulator/dmd_game.py b/dmd_simulator/dmd_game.py new file mode 100644 index 0000000..4d6680f --- /dev/null +++ b/dmd_simulator/dmd_game.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +import argparse +import sdl2 +from dmd_simulator.dmd_player import DmdPlayer +from dmd_simulator.dmd_font import DmdFont +from importlib import resources as impresources +import time +from PIL import Image +import threading +from websockets.sync.server import serve + + + +class Controller: + right_was_pressed=False + left_was_pressed=False + pressing_right=False + pressing_left=False + + def __init__(self): + pass + + def pollEvent(self): + pass + + def stop(self): + pass + + def RightWasPressed(self): + if self.right_was_pressed: + self.right_was_pressed=False + return True + else: + return False + + def LeftWasPressed(self): + if self.left_was_pressed: + self.left_was_pressed=False + return True + else: + return False + +class GamePadController(Controller): + + joysticks = {} + + def __init__(self): + sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER) + + def pollEvent(self): + #return super().pollEvent() + event = sdl2.SDL_Event() + while sdl2.SDL_PollEvent(event) != 0: + # Joystick added/removed + if event.type == sdl2.SDL_CONTROLLERDEVICEADDED: + joy = sdl2.SDL_GameControllerOpen(event.cdevice.which) + if joy: + self.joysticks[event.cdevice.which] = joy + if event.type == sdl2.SDL_CONTROLLERDEVICEREMOVED: + del self.joysticks[event.cdevice.which] + if event.type == sdl2.SDL_JOYDEVICEADDED: + if sdl2.SDL_IsGameController(event.jdevice.which) == False: + joy = sdl2.SDL_JoystickOpen(event.jdevice.which) + if joy: + self.joysticks[event.jdevice.which] = joy + if event.type == sdl2.SDL_JOYDEVICEREMOVED: + if sdl2.SDL_IsGameController(event.jdevice.which) == False: + del self.joysticks[event.jdevice.which] + + # If some arrows did pressed - change direction + if (event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT): + if (not self.pressing_left): + self.left_was_pressed=True + self.pressing_left=True + if (event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT): + if (not self.pressing_right): + self.right_was_pressed=True + self.pressing_right=True + + if event.type == sdl2.SDL_CONTROLLERBUTTONUP: + if event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT: + self.pressing_left=False + if event.cbutton.button == sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + self.pressing_right=False + + if event.type == sdl2.SDL_JOYHATMOTION: + if sdl2.SDL_IsGameController(event.jdevice.which) == False: + if event.jhat.hat == 0: + if event.jhat.value == 8: # left + self.pressing_left=True + self.pressing_right=False + self.left_was_pressed=True + elif event.jhat.value == 2: # right + self.pressing_right=True + self.pressing_left=False + self.right_was_pressed=True + elif event.jhat.value == 1 or event.jhat.value == 0: + self.pressing_left=False + self.pressing_right=False + def stop(self): + for joystick in self.joysticks: + if sdl2.SDL_IsGameController(joystick): + sdl2.SDL_GameControllerClose(self.joysticks[joystick]) + else: + sdl2.SDL_JoystickClose(self.joysticks[joystick]) + +class WSContoller(Controller,threading.Thread): + def __init__(self,host,port): + threading.Thread.__init__(self) + self.host=host + self.port=port + self.start() + + def run(self): + with serve(self.get_event, self.host, self.port) as server: + server.serve_forever() + + def get_event(self,websocket): + for message in websocket: + if message=="right": + self.right_was_pressed=True + elif message=="left": + self.left_was_pressed=True + +class DmdGame: + def __init__(self,game_name,font_ld,font_hd): + # Parsing configuration + parser = argparse.ArgumentParser(prog="dmd_"+game_name+".py") + self.args(parser) + parser.parse_args(namespace=self) + if self.hd: + self.width = 256 + self.height = 64 + self.font=DmdFont(font_hd) + else: + self.font=DmdFont(font_ld) + self.game_name=game_name + # Connecting DMD Server + self.layer=self.game_name + self.client=DmdPlayer.connect(self) + self.header = DmdPlayer.getHeader(self.width, self.height, self.layer, self.width * self.height * 2) # RGB565 + # Display Splash Screen or Not + if (not self.nosplashscreen): + dmdresroot = impresources.files("dmd_simulator") + dmdrespath = dmdresroot.joinpath("data/"+self.game_name) + DmdPlayer.sendImageFile(self.header,self.client,0,dmdrespath.joinpath("dmd-"+self.game_name+".png"),self.width,self.height,True) + time.sleep(self.splashscreendelay) + if (self.enablews): + self.controller=WSContoller(self.wshost,self.wsport) + else: + self.controller=GamePadController() + + def newFrame(self): + self.frame = Image.new("RGBA", (self.width, self.height)) + + def sendFrame(self): + DmdPlayer.sendFrame(self.header, self.client, self.layer, DmdPlayer.imageConvert(self.frame, True)) + + def drawScore(self): + pass + + def drawGameOver(self): + pass + + def displayScore(self): + if (not self.noscoredisplay): + self.drawScore() + + def displayGameOver(self): + if (not self.noscoredisplay): + #self.newFrame() + self.drawScore() + self.drawGameOver() + self.sendFrame() + time.sleep(self.splashscreendelay) + + def end(self): + self.controller.stop() + self.client.close() + + def args(self,parser): + parser.add_argument("-p", "--port", type=int, default=6789, help="network connexion port (default 6789)") + parser.add_argument("--host", default="localhost", help="dmd server host (default localhost)") + parser.add_argument("--width", type=int, default=128, help="dmd width (default 128)") + parser.add_argument("--height", type=int, default= 32, help="dmd height (default 32)") + parser.add_argument("--hd", action="store_true", help="hd format, equivalent of --width 256 --height 64") + parser.add_argument("--noscoredisplay", action="store_true", help="don't display score") + parser.add_argument("--nosplashscreen", action="store_true", help="don't display the splashscreen") + parser.add_argument("--splashscreendelay",type=int, default=2, help="time to display the splashscreen(default 2s)") + parser.add_argument("--enablews", action="store_true", help="enable WebSocket remote control") + parser.add_argument("--wshost", default="0.0.0.0", help="WebSocket host (default 0.0.0.0)") + parser.add_argument("--wsport", type=int, default=8080, help="WebSocket port (default 8080)") + +