From a076d498b1b8d6a80cbc2037620601fb4a7abddf Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 28 Sep 2023 14:31:37 -0700 Subject: [PATCH 1/5] snakegame uses a background task --- snakegame/snakegame/snakegame.py | 148 ++++++++++++++++++------------- 1 file changed, 85 insertions(+), 63 deletions(-) diff --git a/snakegame/snakegame/snakegame.py b/snakegame/snakegame/snakegame.py index e23df403..ffd0c925 100644 --- a/snakegame/snakegame/snakegame.py +++ b/snakegame/snakegame/snakegame.py @@ -12,8 +12,8 @@ HEAD_R = "R" class State(rx.State): - tmpdir:str = HEAD_R dir:str = HEAD_R # direction of what head of snake face + moves:list[str] = [] snake:list[list[int]] = [[10,10], [10,11],[10,12],[10,13],[10,14],[10,15]] # all (X,Y) for snake's body food:list[int] = [5,5] # X, Y of food cells:list[str] = (N*N)*[COLOR_NONE] @@ -21,87 +21,105 @@ class State(rx.State): score: int = 0 magic:int = 1 rate:int = 10 - start: bool = False + running: bool = False + message: str = "" + _n_tasks: int = 0 + def turnOnTick(self): - self.start = True - if self.start: - #print(self.snake) - #print(self.food) - return State.tick + if not self.running: + self.running = True + return State.loop def turnOffTick(self): - self.start = False - if self.start: - return State.tick + self.running = False + def flip_switch(self, start): - self.start = start - if self.start: - return State.tick + if self.running: + return State.turnOffTick + else: + return State.turnOnTick - async def tick(self): - if self.start: + @rx.background + async def loop(self): + async with self: + if self._n_tasks > 0: + return + self._n_tasks += 1 + self.message = "" + + while self.running: + print(f"TICK: {self.tick_cnt}") await asyncio.sleep(0.5) - self.dir = self.tmpdir - head = self.snake[-1].copy() - if(self.dir==HEAD_U): + async with self: + head = self.snake[-1].copy() + # XXX: hack needed until #1876 merges + dir = self.moves[0] if self.moves else self.dir + self.moves.pop(0) if self.moves else None + self.dir = dir + # XXX: end hack (should just use `.pop(0)`) + print(head, dir) + if(dir==HEAD_U): head[1] += (N-1) head[1] %= N - elif(self.dir==HEAD_D): + elif(dir==HEAD_D): head[1] += (N+1) head[1] %= N - elif(self.dir==HEAD_L): + elif(dir==HEAD_L): head[0] += (N-1) head[0] %= N - elif(self.dir==HEAD_R): + elif(dir==HEAD_R): head[0] += (N+1) head[0] %= N - if(head in self.snake): - self.start = False - self.magic = 1 - for i in range(N*N): - self.cells[i] = COLOR_NONE - self.snake = [[10,10], [10,11],[10,12],[10,13],[10,14],[10,15]].copy() - self.food = [5,5] - self.dir = HEAD_R - await asyncio.sleep(3) - return State.tick - - self.cells[head[0]+ N*head[1]] = COLOR_BODY - self.snake.append(head.copy()) - FOOD_EATEN = False - while(self.food in self.snake): - FOOD_EATEN = True - self.food = [random.randint(0,N-1), random.randint(0,N-1)] - self.cells[self.food[0]+ N*self.food[1]] = COLOR_FOOD - if(FOOD_EATEN==False): - self.cells[self.snake[0][0]+ N*self.snake[0][1]] = COLOR_NONE - del self.snake[0] - else: - self.score+=self.magic - self.magic += 1 - #self.rate = (int)(100*((float)self.score / (float)self.tick_cnt)) - self.rate = (int)(100*((float)(self.score)/(float)(self.tick_cnt))) - self.tick_cnt += 1 - #print(self.tick_cnt) + async with self: + if head in self.snake: + self.message = "GAME OVER" + self.running = False + self.magic = 1 + for i in range(N*N): + self.cells[i] = COLOR_NONE + self.snake = [[10,10], [10,11],[10,12],[10,13],[10,14],[10,15]].copy() + self.food = [5,5] + self.dir = HEAD_R + break + + # Move the snake + async with self: + self.cells[head[0]+ N*head[1]] = COLOR_BODY + self.snake.append(head.copy()) + FOOD_EATEN = False + while(self.food in self.snake): + FOOD_EATEN = True + self.food = [random.randint(0,N-1), random.randint(0,N-1)] + self.cells[self.food[0]+ N*self.food[1]] = COLOR_FOOD + if(FOOD_EATEN==False): + self.cells[self.snake[0][0]+ N*self.snake[0][1]] = COLOR_NONE + del self.snake[0] + else: + self.score+=self.magic + self.magic += 1 + self.rate = (int)(100*((float)(self.score)/(float)(self.tick_cnt))) + self.tick_cnt += 1 + + async with self: + self._n_tasks -= 1 - return State.tick def arrow_up(self): - if(self.dir != HEAD_D): - self.tmpdir = HEAD_U - return + if((self.moves[-1] if self.moves else self.dir) != HEAD_D): + self.moves.append(HEAD_U) + def arrow_left(self): - if(self.dir != HEAD_R): - self.tmpdir = HEAD_L - return + if((self.moves[-1] if self.moves else self.dir) != HEAD_R): + self.moves.append(HEAD_L) + def arrow_right(self): - if(self.dir != HEAD_L): - self.tmpdir = HEAD_R - return + if((self.moves[-1] if self.moves else self.dir) != HEAD_L): + self.moves.append(HEAD_R) + def arrow_down(self): - if(self.dir != HEAD_U): - self.tmpdir = HEAD_D - return + if((self.moves[-1] if self.moves else self.dir) != HEAD_U): + self.moves.append(HEAD_D) + def arrow_none(self): return @@ -113,7 +131,7 @@ def index(): rx.hstack( rx.button("PAUSE", on_click=State.turnOffTick, color_scheme="blue", border_radius="1em"), rx.button("RUN", on_click=State.turnOnTick, color_scheme="green", border_radius="1em"), - rx.switch(is_checked=State.start, on_change=State.flip_switch), + rx.switch(is_checked=State.running, on_change=State.flip_switch), ), rx.hstack( @@ -145,6 +163,10 @@ def index(): padding_right="1em", ), ), + rx.cond( + State.message, + rx.heading(State.message) + ), # Usage of foreach, please refer https://reflex.app/docs/library/layout/foreach rx.responsive_grid( rx.foreach( From 4ef11c6931712e6e31f2fd3e157e344756f0fb94 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 28 Sep 2023 16:22:47 -0700 Subject: [PATCH 2/5] snakegame: keyboard controls --- snakegame/snakegame/snakegame.py | 94 ++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 11 deletions(-) diff --git a/snakegame/snakegame/snakegame.py b/snakegame/snakegame/snakegame.py index ffd0c925..6033bd67 100644 --- a/snakegame/snakegame/snakegame.py +++ b/snakegame/snakegame/snakegame.py @@ -1,3 +1,4 @@ +from typing import Any, Dict, List, Optional import reflex as rx import asyncio import random @@ -6,11 +7,13 @@ COLOR_NONE="#EEEEEE" COLOR_BODY="#008800" COLOR_FOOD="#FF00FF" +COLOR_DEAD="#FF0000" HEAD_U = "U" HEAD_D = "D" HEAD_L = "L" HEAD_R = "R" + class State(rx.State): dir:str = HEAD_R # direction of what head of snake face moves:list[str] = [] @@ -22,7 +25,7 @@ class State(rx.State): magic:int = 1 rate:int = 10 running: bool = False - message: str = "" + died: bool = False _n_tasks: int = 0 def turnOnTick(self): @@ -45,7 +48,15 @@ async def loop(self): if self._n_tasks > 0: return self._n_tasks += 1 - self.message = "" + if self.died: + self.died = False + self.magic = 1 + for i in range(N*N): + self.cells[i] = COLOR_NONE + self.snake = [[10,10], [10,11],[10,12],[10,13],[10,14],[10,15]].copy() + self.food = [5,5] + self.dir = HEAD_R + self.moves = [] while self.running: print(f"TICK: {self.tick_cnt}") @@ -72,14 +83,9 @@ async def loop(self): head[0] %= N async with self: if head in self.snake: - self.message = "GAME OVER" self.running = False - self.magic = 1 - for i in range(N*N): - self.cells[i] = COLOR_NONE - self.snake = [[10,10], [10,11],[10,12],[10,13],[10,14],[10,15]].copy() - self.food = [5,5] - self.dir = HEAD_R + self.died = True + self.cells[head[0]+ N*head[1]] = COLOR_DEAD break # Move the snake @@ -123,6 +129,71 @@ def arrow_down(self): def arrow_none(self): return + def arrow_rel_left(self): + last_dir = self.moves[-1] if self.moves else self.dir + if last_dir == HEAD_U: + self.arrow_left() + elif last_dir == HEAD_L: + self.arrow_down() + elif last_dir == HEAD_D: + self.arrow_right() + elif last_dir == HEAD_R: + self.arrow_up() + + def arrow_rel_right(self): + last_dir = self.moves[-1] if self.moves else self.dir + if last_dir == HEAD_U: + self.arrow_right() + elif last_dir == HEAD_L: + self.arrow_up() + elif last_dir == HEAD_D: + self.arrow_left() + elif last_dir == HEAD_R: + self.arrow_down() + + def handle_key(self, key): + if key == "ArrowUp": + self.arrow_up() + elif key == "ArrowLeft": + self.arrow_left() + elif key == "ArrowRight": + self.arrow_right() + elif key == "ArrowDown": + self.arrow_down() + elif key == ",": + self.arrow_rel_left() + elif key == ".": + self.arrow_rel_right() + else: + print(key) + + +class GlobalKeyWatcher(rx.Fragment): + # List of keys to trigger on + keys: rx.vars.Var[List[str]] = [] + + def _get_hooks(self) -> str | None: + return """ +useEffect(() => { + const handle_key = (_e0) => { + if (%s.includes(_e0.key)) + %s + } + document.addEventListener("keydown", handle_key, false); + return () => { + document.removeEventListener("keydown", handle_key, false); + } +}) +""" % (self.keys, rx.utils.format.format_event_chain(self.event_triggers["on_key_down"])) + def get_event_triggers(self) -> Dict[str, Any]: + return { + "on_key_down": lambda e0: [e0.key], + } + + def render(self) -> str: + return "" + + def colored_box(color, index): return rx.box(bg=color, width="1em", height="1em", border="1px solid white") @@ -164,8 +235,8 @@ def index(): ), ), rx.cond( - State.message, - rx.heading(State.message) + State.died, + rx.heading("Game Over 🐍") ), # Usage of foreach, please refer https://reflex.app/docs/library/layout/foreach rx.responsive_grid( @@ -175,6 +246,7 @@ def index(): ), columns=[N], ), + GlobalKeyWatcher.create(keys=["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown", ",", "."], on_key_down=State.handle_key), rx.hstack( rx.vstack( rx.button("○", on_click=State.arrow_none, color_scheme="#FFFFFFFF", border_radius="1em",font_size="2em"), From 4b851b87a4ed4e349488a512dd64166ee763039f Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 28 Sep 2023 16:23:11 -0700 Subject: [PATCH 3/5] snakegame: blacken --- snakegame/snakegame/snakegame.py | 177 +++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 57 deletions(-) diff --git a/snakegame/snakegame/snakegame.py b/snakegame/snakegame/snakegame.py index 6033bd67..5ff5f69c 100644 --- a/snakegame/snakegame/snakegame.py +++ b/snakegame/snakegame/snakegame.py @@ -3,11 +3,11 @@ import asyncio import random -N = 19 # There is a N*N grid for ground of snake -COLOR_NONE="#EEEEEE" -COLOR_BODY="#008800" -COLOR_FOOD="#FF00FF" -COLOR_DEAD="#FF0000" +N = 19 # There is a N*N grid for ground of snake +COLOR_NONE = "#EEEEEE" +COLOR_BODY = "#008800" +COLOR_FOOD = "#FF00FF" +COLOR_DEAD = "#FF0000" HEAD_U = "U" HEAD_D = "D" HEAD_L = "L" @@ -15,15 +15,22 @@ class State(rx.State): - dir:str = HEAD_R # direction of what head of snake face - moves:list[str] = [] - snake:list[list[int]] = [[10,10], [10,11],[10,12],[10,13],[10,14],[10,15]] # all (X,Y) for snake's body - food:list[int] = [5,5] # X, Y of food - cells:list[str] = (N*N)*[COLOR_NONE] - tick_cnt:int = 1 + dir: str = HEAD_R # direction of what head of snake face + moves: list[str] = [] + snake: list[list[int]] = [ + [10, 10], + [10, 11], + [10, 12], + [10, 13], + [10, 14], + [10, 15], + ] # all (X,Y) for snake's body + food: list[int] = [5, 5] # X, Y of food + cells: list[str] = (N * N) * [COLOR_NONE] + tick_cnt: int = 1 score: int = 0 - magic:int = 1 - rate:int = 10 + magic: int = 1 + rate: int = 10 running: bool = False died: bool = False _n_tasks: int = 0 @@ -51,10 +58,17 @@ async def loop(self): if self.died: self.died = False self.magic = 1 - for i in range(N*N): + for i in range(N * N): self.cells[i] = COLOR_NONE - self.snake = [[10,10], [10,11],[10,12],[10,13],[10,14],[10,15]].copy() - self.food = [5,5] + self.snake = [ + [10, 10], + [10, 11], + [10, 12], + [10, 13], + [10, 14], + [10, 15], + ].copy() + self.food = [5, 5] self.dir = HEAD_R self.moves = [] @@ -69,61 +83,62 @@ async def loop(self): self.dir = dir # XXX: end hack (should just use `.pop(0)`) print(head, dir) - if(dir==HEAD_U): - head[1] += (N-1) + if dir == HEAD_U: + head[1] += N - 1 head[1] %= N - elif(dir==HEAD_D): - head[1] += (N+1) + elif dir == HEAD_D: + head[1] += N + 1 head[1] %= N - elif(dir==HEAD_L): - head[0] += (N-1) + elif dir == HEAD_L: + head[0] += N - 1 head[0] %= N - elif(dir==HEAD_R): - head[0] += (N+1) + elif dir == HEAD_R: + head[0] += N + 1 head[0] %= N async with self: if head in self.snake: self.running = False self.died = True - self.cells[head[0]+ N*head[1]] = COLOR_DEAD + self.cells[head[0] + N * head[1]] = COLOR_DEAD break # Move the snake async with self: - self.cells[head[0]+ N*head[1]] = COLOR_BODY + self.cells[head[0] + N * head[1]] = COLOR_BODY self.snake.append(head.copy()) FOOD_EATEN = False - while(self.food in self.snake): + while self.food in self.snake: FOOD_EATEN = True - self.food = [random.randint(0,N-1), random.randint(0,N-1)] - self.cells[self.food[0]+ N*self.food[1]] = COLOR_FOOD - if(FOOD_EATEN==False): - self.cells[self.snake[0][0]+ N*self.snake[0][1]] = COLOR_NONE + self.food = [random.randint(0, N - 1), random.randint(0, N - 1)] + self.cells[self.food[0] + N * self.food[1]] = COLOR_FOOD + if FOOD_EATEN == False: + self.cells[self.snake[0][0] + N * self.snake[0][1]] = COLOR_NONE del self.snake[0] else: - self.score+=self.magic + self.score += self.magic self.magic += 1 - self.rate = (int)(100*((float)(self.score)/(float)(self.tick_cnt))) + self.rate = (int)( + 100 * ((float)(self.score) / (float)(self.tick_cnt)) + ) self.tick_cnt += 1 async with self: self._n_tasks -= 1 - def arrow_up(self): - if((self.moves[-1] if self.moves else self.dir) != HEAD_D): + if (self.moves[-1] if self.moves else self.dir) != HEAD_D: self.moves.append(HEAD_U) def arrow_left(self): - if((self.moves[-1] if self.moves else self.dir) != HEAD_R): + if (self.moves[-1] if self.moves else self.dir) != HEAD_R: self.moves.append(HEAD_L) def arrow_right(self): - if((self.moves[-1] if self.moves else self.dir) != HEAD_L): + if (self.moves[-1] if self.moves else self.dir) != HEAD_L: self.moves.append(HEAD_R) def arrow_down(self): - if((self.moves[-1] if self.moves else self.dir) != HEAD_U): + if (self.moves[-1] if self.moves else self.dir) != HEAD_U: self.moves.append(HEAD_D) def arrow_none(self): @@ -184,12 +199,16 @@ def _get_hooks(self) -> str | None: document.removeEventListener("keydown", handle_key, false); } }) -""" % (self.keys, rx.utils.format.format_event_chain(self.event_triggers["on_key_down"])) +""" % ( + self.keys, + rx.utils.format.format_event_chain(self.event_triggers["on_key_down"]), + ) + def get_event_triggers(self) -> Dict[str, Any]: return { "on_key_down": lambda e0: [e0.key], } - + def render(self) -> str: return "" @@ -197,14 +216,24 @@ def render(self) -> str: def colored_box(color, index): return rx.box(bg=color, width="1em", height="1em", border="1px solid white") + def index(): return rx.vstack( rx.hstack( - rx.button("PAUSE", on_click=State.turnOffTick, color_scheme="blue", border_radius="1em"), - rx.button("RUN", on_click=State.turnOnTick, color_scheme="green", border_radius="1em"), + rx.button( + "PAUSE", + on_click=State.turnOffTick, + color_scheme="blue", + border_radius="1em", + ), + rx.button( + "RUN", + on_click=State.turnOnTick, + color_scheme="green", + border_radius="1em", + ), rx.switch(is_checked=State.running, on_change=State.flip_switch), ), - rx.hstack( rx.vstack( rx.heading("RATE", font_size="1em"), @@ -213,9 +242,7 @@ def index(): border_width="1px", padding_left="1em", padding_right="1em", - ), - rx.vstack( rx.heading("SCORE", font_size="1em"), rx.heading(State.score, font_size="2em"), @@ -223,7 +250,6 @@ def index(): border_width="1px", padding_left="1em", padding_right="1em", - ), rx.vstack( rx.heading("MAGIC", font_size="1em"), @@ -234,10 +260,7 @@ def index(): padding_right="1em", ), ), - rx.cond( - State.died, - rx.heading("Game Over 🐍") - ), + rx.cond(State.died, rx.heading("Game Over 🐍")), # Usage of foreach, please refer https://reflex.app/docs/library/layout/foreach rx.responsive_grid( rx.foreach( @@ -246,24 +269,64 @@ def index(): ), columns=[N], ), - GlobalKeyWatcher.create(keys=["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown", ",", "."], on_key_down=State.handle_key), + GlobalKeyWatcher.create( + keys=["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown", ",", "."], + on_key_down=State.handle_key, + ), rx.hstack( rx.vstack( - rx.button("○", on_click=State.arrow_none, color_scheme="#FFFFFFFF", border_radius="1em",font_size="2em"), - rx.button("←", on_click=State.arrow_left, color_scheme="red", border_radius="1em",font_size="2em"), + rx.button( + "○", + on_click=State.arrow_none, + color_scheme="#FFFFFFFF", + border_radius="1em", + font_size="2em", + ), + rx.button( + "←", + on_click=State.arrow_left, + color_scheme="red", + border_radius="1em", + font_size="2em", + ), ), rx.vstack( - rx.button("↑", on_click=State.arrow_up, color_scheme="red", border_radius="1em",font_size="2em"), - rx.button("↓", on_click=State.arrow_down, color_scheme="red", border_radius="1em",font_size="2em"), + rx.button( + "↑", + on_click=State.arrow_up, + color_scheme="red", + border_radius="1em", + font_size="2em", + ), + rx.button( + "↓", + on_click=State.arrow_down, + color_scheme="red", + border_radius="1em", + font_size="2em", + ), ), rx.vstack( - rx.button("○", on_click=State.arrow_none, color_scheme="#FFFFFFFF", border_radius="1em",font_size="2em"), - rx.button("→", on_click=State.arrow_right, color_scheme="red", border_radius="1em",font_size="2em"), + rx.button( + "○", + on_click=State.arrow_none, + color_scheme="#FFFFFFFF", + border_radius="1em", + font_size="2em", + ), + rx.button( + "→", + on_click=State.arrow_right, + color_scheme="red", + border_radius="1em", + font_size="2em", + ), ), ), padding_top="3%", ) + app = rx.App(state=State) app.add_page(index, title="snake game") From 9267d6beac304c86cdc0dde9c675563a39c634bb Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 29 Sep 2023 15:03:58 -0700 Subject: [PATCH 4/5] snakegame: reformat and clean up code Add docstrings and comments Separate UI into sub functions Move defaults and constants to module level --- snakegame/requirements.txt | 4 +- snakegame/snakegame/snakegame.py | 393 ++++++++++++++++--------------- 2 files changed, 204 insertions(+), 193 deletions(-) diff --git a/snakegame/requirements.txt b/snakegame/requirements.txt index 210218f4..e303bd41 100644 --- a/snakegame/requirements.txt +++ b/snakegame/requirements.txt @@ -1 +1,3 @@ -reflex>=0.2.0 \ No newline at end of file +#reflex>=0.2.9 +# temporary until state.reset() bug is fixed +git+https://github.com/reflex-dev/reflex@masenf/state-reset-deepcopy diff --git a/snakegame/snakegame/snakegame.py b/snakegame/snakegame/snakegame.py index 5ff5f69c..b5e4db0e 100644 --- a/snakegame/snakegame/snakegame.py +++ b/snakegame/snakegame/snakegame.py @@ -1,189 +1,203 @@ -from typing import Any, Dict, List, Optional -import reflex as rx import asyncio import random +from typing import Any, Dict, List + +import reflex as rx N = 19 # There is a N*N grid for ground of snake COLOR_NONE = "#EEEEEE" COLOR_BODY = "#008800" COLOR_FOOD = "#FF00FF" COLOR_DEAD = "#FF0000" -HEAD_U = "U" -HEAD_D = "D" -HEAD_L = "L" -HEAD_R = "R" +# Tuples representing the directions the snake head can move +HEAD_U = (0, -1) +HEAD_D = (0, 1) +HEAD_L = (-1, 0) +HEAD_R = (1, 0) +INITIAL_SNAKE = [ # all (X,Y) for snake's body + (-1, -1), + (-1, -1), + (-1, -1), + (-1, -1), + (-1, -1), + (10, 15), # Starting head position +] +INITIAL_FOOD = (5, 5) # X, Y of food + + +def get_new_head(old_head: tuple[int, int], dir: tuple[int, int]) -> tuple[int, int]: + """Calculate the new head position based on the given direction.""" + x, y = old_head + return (x + dir[0] + N) % N, (y + dir[1] + N) % N + + +def to_cell_index(x: int, y: int) -> int: + """Calculate the index into the game board for the given (X, Y).""" + return x + N * y class State(rx.State): - dir: str = HEAD_R # direction of what head of snake face - moves: list[str] = [] - snake: list[list[int]] = [ - [10, 10], - [10, 11], - [10, 12], - [10, 13], - [10, 14], - [10, 15], - ] # all (X,Y) for snake's body - food: list[int] = [5, 5] # X, Y of food - cells: list[str] = (N * N) * [COLOR_NONE] - tick_cnt: int = 1 - score: int = 0 - magic: int = 1 - rate: int = 10 + dir: str = HEAD_R # Direction the snake head is facing currently + moves: list[tuple[int, int]] = [] # Queue of moves based on user input + snake: list[tuple[int, int]] = INITIAL_SNAKE # Body of snake + food: tuple[int, int] = INITIAL_FOOD # X, Y location of food + cells: list[str] = (N * N) * [COLOR_NONE] # The game board to be rendered + score: int = 0 # Player score + magic: int = 1 # Number of points per food eaten + rate: int = 10 # 5 divide by rate determines tick period + died: bool = False # If the snake is dead (game over) + tick_cnt: int = 1 # How long the game has been running running: bool = False - died: bool = False _n_tasks: int = 0 - def turnOnTick(self): + def play(self): + """Start / resume the game.""" if not self.running: + if self.died: + # If the player is dead, reset game state before beginning. + self.reset() self.running = True return State.loop - def turnOffTick(self): + def pause(self): + """Signal the game to pause.""" self.running = False def flip_switch(self, start): - if self.running: - return State.turnOffTick + """Toggle whether the game is running or paused.""" + if start: + return State.play else: - return State.turnOnTick + return State.pause + + def _next_move(self): + """Returns the next direction the snake head should move in.""" + return self.moves[0] if self.moves else self.dir + + def _last_move(self): + """Returns the last queued direction the snake head should move in.""" + return self.moves[-1] if self.moves else self.dir @rx.background async def loop(self): + """The main game loop, implemented as a singleton background task. + + Responsible for updating the game state on each tick. + """ async with self: if self._n_tasks > 0: + # Only start one loop task at a time. return self._n_tasks += 1 - if self.died: - self.died = False - self.magic = 1 - for i in range(N * N): - self.cells[i] = COLOR_NONE - self.snake = [ - [10, 10], - [10, 11], - [10, 12], - [10, 13], - [10, 14], - [10, 15], - ].copy() - self.food = [5, 5] - self.dir = HEAD_R - self.moves = [] while self.running: - print(f"TICK: {self.tick_cnt}") - await asyncio.sleep(0.5) - async with self: - head = self.snake[-1].copy() - # XXX: hack needed until #1876 merges - dir = self.moves[0] if self.moves else self.dir - self.moves.pop(0) if self.moves else None - self.dir = dir - # XXX: end hack (should just use `.pop(0)`) - print(head, dir) - if dir == HEAD_U: - head[1] += N - 1 - head[1] %= N - elif dir == HEAD_D: - head[1] += N + 1 - head[1] %= N - elif dir == HEAD_L: - head[0] += N - 1 - head[0] %= N - elif dir == HEAD_R: - head[0] += N + 1 - head[0] %= N + # Sleep based on the current rate + await asyncio.sleep(5 / self.rate) async with self: + # Which direction will the snake move? + self.dir = self._next_move() + if self.moves: + # Remove the processed next move from the queue + del self.moves[0] + + # Calculate new head position + head = get_new_head(self.snake[-1], dir=self.dir) if head in self.snake: + # New head position crashes into snake body, Game Over + print(head, self.snake) self.running = False self.died = True - self.cells[head[0] + N * head[1]] = COLOR_DEAD + self.cells[to_cell_index(*head)] = COLOR_DEAD break - # Move the snake - async with self: - self.cells[head[0] + N * head[1]] = COLOR_BODY - self.snake.append(head.copy()) - FOOD_EATEN = False + # Move the snake + self.snake.append(head) + self.cells[to_cell_index(*head)] = COLOR_BODY + food_eaten = False while self.food in self.snake: - FOOD_EATEN = True - self.food = [random.randint(0, N - 1), random.randint(0, N - 1)] - self.cells[self.food[0] + N * self.food[1]] = COLOR_FOOD - if FOOD_EATEN == False: - self.cells[self.snake[0][0] + N * self.snake[0][1]] = COLOR_NONE + food_eaten = True + self.food = (random.randint(0, N - 1), random.randint(0, N - 1)) + self.cells[to_cell_index(*self.food)] = COLOR_FOOD + if not food_eaten: + # Advance the snake + self.cells[to_cell_index(*self.snake[0])] = COLOR_NONE del self.snake[0] else: + # Grow the snake (and the score) self.score += self.magic self.magic += 1 - self.rate = (int)( - 100 * ((float)(self.score) / (float)(self.tick_cnt)) - ) + self.rate = 10 + self.magic self.tick_cnt += 1 async with self: + # Decrement task counter, since we're about to return self._n_tasks -= 1 def arrow_up(self): - if (self.moves[-1] if self.moves else self.dir) != HEAD_D: + """Queue a move up.""" + if self._last_move() != HEAD_D: self.moves.append(HEAD_U) def arrow_left(self): - if (self.moves[-1] if self.moves else self.dir) != HEAD_R: + """Queue a move left.""" + if self._last_move() != HEAD_R: self.moves.append(HEAD_L) def arrow_right(self): - if (self.moves[-1] if self.moves else self.dir) != HEAD_L: + """Queue a move right.""" + if self._last_move() != HEAD_L: self.moves.append(HEAD_R) def arrow_down(self): - if (self.moves[-1] if self.moves else self.dir) != HEAD_U: + """Queue a move down.""" + if self._last_move() != HEAD_U: self.moves.append(HEAD_D) - def arrow_none(self): - return - def arrow_rel_left(self): - last_dir = self.moves[-1] if self.moves else self.dir - if last_dir == HEAD_U: + """Queue a move left relative to the current direction.""" + last_move = self._last_move() + if last_move == HEAD_U: self.arrow_left() - elif last_dir == HEAD_L: + elif last_move == HEAD_L: self.arrow_down() - elif last_dir == HEAD_D: + elif last_move == HEAD_D: self.arrow_right() - elif last_dir == HEAD_R: + elif last_move == HEAD_R: self.arrow_up() def arrow_rel_right(self): - last_dir = self.moves[-1] if self.moves else self.dir - if last_dir == HEAD_U: + """Queue a move right relative to the current direction.""" + last_move = self._last_move() + if last_move == HEAD_U: self.arrow_right() - elif last_dir == HEAD_L: + elif last_move == HEAD_L: self.arrow_up() - elif last_dir == HEAD_D: + elif last_move == HEAD_D: self.arrow_left() - elif last_dir == HEAD_R: + elif last_move == HEAD_R: self.arrow_down() def handle_key(self, key): - if key == "ArrowUp": - self.arrow_up() - elif key == "ArrowLeft": - self.arrow_left() - elif key == "ArrowRight": - self.arrow_right() - elif key == "ArrowDown": - self.arrow_down() - elif key == ",": - self.arrow_rel_left() - elif key == ".": - self.arrow_rel_right() - else: - print(key) + """Handle keyboard press.""" + { + "ArrowUp": self.arrow_up, + "ArrowLeft": self.arrow_left, + "ArrowRight": self.arrow_right, + "ArrowDown": self.arrow_down, + ",": self.arrow_rel_left, + ".": self.arrow_rel_right, + }[key]() class GlobalKeyWatcher(rx.Fragment): + """A component that attaches a keydown handler to the document. + + The handler only calls the backend function if the pressed key is one of the + specified keys. + + Requires custom javascript to support this functionality at the moment. + """ + # List of keys to trigger on keys: rx.vars.Var[List[str]] = [] @@ -210,57 +224,104 @@ def get_event_triggers(self) -> Dict[str, Any]: } def render(self) -> str: + # This component has no visual element. return "" def colored_box(color, index): + """One square of the game grid.""" return rx.box(bg=color, width="1em", height="1em", border="1px solid white") +def stat_box(label, value): + """One of the score, magic, or rate boxes.""" + return rx.vstack( + rx.heading(label, font_size="1em"), + rx.heading(value, font_size="2em"), + bg_color="yellow", + border_width="1px", + padding_left="1em", + padding_right="1em", + ) + + +def control_button(label, on_click): + """One of the arrow buttons for touch/mouse control.""" + return rx.button( + label, + on_click=on_click, + color_scheme="red", + border_radius="1em", + font_size="2em", + ) + + +def padding_button(): + """A button that is used for padding in the controls panel.""" + return rx.button( + "○", + color_scheme="#FFFFFF", + border_radius="1em", + font_size="2em", + ) + + +def controls_panel(): + """The controls panel of arrow buttons.""" + return rx.hstack( + GlobalKeyWatcher.create( + keys=["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown", ",", "."], + on_key_down=State.handle_key, + ), + rx.vstack( + padding_button(), + control_button( + "←", + on_click=State.arrow_left, + ), + ), + rx.vstack( + control_button( + "↑", + on_click=State.arrow_up, + ), + control_button( + "↓", + on_click=State.arrow_down, + ), + ), + rx.vstack( + padding_button(), + control_button( + "→", + on_click=State.arrow_right, + ), + ), + ) + + def index(): return rx.vstack( rx.hstack( rx.button( "PAUSE", - on_click=State.turnOffTick, + on_click=State.pause, color_scheme="blue", border_radius="1em", ), rx.button( "RUN", - on_click=State.turnOnTick, + on_click=State.play, color_scheme="green", border_radius="1em", ), rx.switch(is_checked=State.running, on_change=State.flip_switch), ), rx.hstack( - rx.vstack( - rx.heading("RATE", font_size="1em"), - rx.heading(State.rate, font_size="2em"), - bg_color="yellow", - border_width="1px", - padding_left="1em", - padding_right="1em", - ), - rx.vstack( - rx.heading("SCORE", font_size="1em"), - rx.heading(State.score, font_size="2em"), - bg_color="yellow", - border_width="1px", - padding_left="1em", - padding_right="1em", - ), - rx.vstack( - rx.heading("MAGIC", font_size="1em"), - rx.heading(State.magic, font_size="2em"), - bg_color="yellow", - border_width="1px", - padding_left="1em", - padding_right="1em", - ), + stat_box("RATE", State.rate), + stat_box("SCORE", State.score), + stat_box("MAGIC", State.magic), ), - rx.cond(State.died, rx.heading("Game Over 🐍")), # Usage of foreach, please refer https://reflex.app/docs/library/layout/foreach rx.responsive_grid( rx.foreach( @@ -269,60 +330,8 @@ def index(): ), columns=[N], ), - GlobalKeyWatcher.create( - keys=["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown", ",", "."], - on_key_down=State.handle_key, - ), - rx.hstack( - rx.vstack( - rx.button( - "○", - on_click=State.arrow_none, - color_scheme="#FFFFFFFF", - border_radius="1em", - font_size="2em", - ), - rx.button( - "←", - on_click=State.arrow_left, - color_scheme="red", - border_radius="1em", - font_size="2em", - ), - ), - rx.vstack( - rx.button( - "↑", - on_click=State.arrow_up, - color_scheme="red", - border_radius="1em", - font_size="2em", - ), - rx.button( - "↓", - on_click=State.arrow_down, - color_scheme="red", - border_radius="1em", - font_size="2em", - ), - ), - rx.vstack( - rx.button( - "○", - on_click=State.arrow_none, - color_scheme="#FFFFFFFF", - border_radius="1em", - font_size="2em", - ), - rx.button( - "→", - on_click=State.arrow_right, - color_scheme="red", - border_radius="1em", - font_size="2em", - ), - ), - ), + rx.cond(State.died, rx.heading("Game Over 🐍")), + controls_panel(), padding_top="3%", ) From c0ee5b53e5def96dcae1d45c0c2c5e23364d49d5 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 10 Oct 2023 11:52:25 -0700 Subject: [PATCH 5/5] Update snakegame/requirements.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thomas Brandého --- snakegame/requirements.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/snakegame/requirements.txt b/snakegame/requirements.txt index e303bd41..c8ade313 100644 --- a/snakegame/requirements.txt +++ b/snakegame/requirements.txt @@ -1,3 +1 @@ -#reflex>=0.2.9 -# temporary until state.reset() bug is fixed -git+https://github.com/reflex-dev/reflex@masenf/state-reset-deepcopy +reflex>=0.2.9