From 428bbae7360915b40e3e49bd04778c79c35f335b Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 4 Dec 2020 16:02:48 +0100 Subject: [PATCH 01/92] Added base files for map generation and main loop for random walk generation --- dungeonbattle/mapgeneration/__init__.py | 0 dungeonbattle/mapgeneration/randomwalk.py | 44 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 dungeonbattle/mapgeneration/__init__.py create mode 100644 dungeonbattle/mapgeneration/randomwalk.py diff --git a/dungeonbattle/mapgeneration/__init__.py b/dungeonbattle/mapgeneration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py new file mode 100644 index 0000000..c0c807d --- /dev/null +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -0,0 +1,44 @@ +from random import choice, random, randint +from dungeonbattle.interfaces import Map, Tile + + class Generator: + + def __init__(self, params): + self.params = params + + def run(self): + width, height = self.params["width"], self.params["height"] + walkers = [Walker(width//2, height//2)] + grid = [[Tile.WALL] * width] * height + count = 0 + while count < self.params["fill"] * width*height: + # because we can't add or remove walkers while looping over the pop + # we need lists to keep track of what will be the walkers for the + # next iteration of the main loop + next_walker_pop = [] + + for walker in walkers: + if grid[walker.y][walker.x] == Tile.WALL: + count += 1 + grid[walker.y][walker.x] = Tile.EMPTY + if random() < self.params["turn_chance"]: + walker.random_turn() + walker.move_in_bounds(width, height) + if random() > self.params["death_chance"]: + next_walker_pop.append(walker) + + # we use a second loop for spliting so we're not bothered by cases + # like a walker not spliting because we hit the population cap even + # though the next one would have died and freed a place + # not a big if it happened though + for walker in walkers: + if len(next_walker_pop) < self.params["max_walkers"]: + if random() < self.params["split_chance"]: + next_walker_pop.append(walker.split()) + walkers = next_walker_pop + + start_x, start_y = randint(0, width), randint(0, height) + while grid[start_y][start_x] != Tile.EMPTY: + start_x, start_y = randint(0, width), randint(0, height) + + return Map(width, height, grid, start_x, start_y) From a5c53c898e90ae3215978f7cd3f2d75f115b529a Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 4 Dec 2020 18:01:54 +0100 Subject: [PATCH 02/92] Implemented walker class and methods random_turn, next_pos, move_in_bounds --- dungeonbattle/mapgeneration/randomwalk.py | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index c0c807d..3c42559 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -1,9 +1,46 @@ +from enum import Enum from random import choice, random, randint from dungeonbattle.interfaces import Map, Tile - class Generator: - def __init__(self, params): + + +class Directions(Enum): + up = auto() + down = auto() + left = auto() + right = auto() + + +class Walker: + + def __init__(self, x, y): + self.x = x + self.y = y + self.dir = choice(list(Directions)) + + def random_turn(self): + self.dir = choice(list(Directions)) + + def next_pos(self): + if self.dir == Directions.up: + return self.x, self.y + 1 + elif self.dir == Directions.down: + return self.x, self.y - 1 + elif self.dir == Directions.right: + return self.x + 1, self.y + elif self.dir == Directions.left: + return self.x - 1, self.y + + def move_in_bounds(self, width, height): + nx, ny = self.next_pos() + if 0 < nx < width and 0 < ny < height: + self.x, self.y = nx, ny + + +class Generator: + + def __init__(self, params = DEFAULT_PARAMS): self.params = params def run(self): From bc9c7cd7f7f30dcd19acc77953d156b3fbff0fc2 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 4 Dec 2020 18:03:41 +0100 Subject: [PATCH 03/92] Finalised implementation of the walker class with method split --- dungeonbattle/mapgeneration/randomwalk.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index 3c42559..f0527b5 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -37,6 +37,11 @@ class Walker: if 0 < nx < width and 0 < ny < height: self.x, self.y = nx, ny + def split(): + child = Walker(self.x, self.y) + child.dir = self.dir + return child + class Generator: From 3717429549dc2e35bfbffb0ebe8bfb0a5b27d33e Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 4 Dec 2020 18:04:50 +0100 Subject: [PATCH 04/92] Added some test default parameters for the random walk generator --- dungeonbattle/mapgeneration/randomwalk.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index f0527b5..35cfa47 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -3,6 +3,13 @@ from random import choice, random, randint from dungeonbattle.interfaces import Map, Tile +DEFAULT_PARAMS = {"split_chance" : .15, + "turn_chance" : .5, + "death_chance" : .1, + "max_walkers" : 15, + "width" : 100, + "height" : 100, + "fill" : .4} class Directions(Enum): From 32e6eab9438abcbb4f05d4440a462358cc5d85a3 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Sun, 6 Dec 2020 23:55:57 +0100 Subject: [PATCH 05/92] Added import enum.auto to mapgeneration.randomwalk --- dungeonbattle/mapgeneration/randomwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index 35cfa47..3dc3906 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import auto, Enum from random import choice, random, randint from dungeonbattle.interfaces import Map, Tile From 2a1be4233b3308afb8a7c9103253c67dbbc28941 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Mon, 7 Dec 2020 00:18:32 +0100 Subject: [PATCH 06/92] Fixed syntax error in Walker.split --- dungeonbattle/mapgeneration/randomwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index 3dc3906..60baaba 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -44,7 +44,7 @@ class Walker: if 0 < nx < width and 0 < ny < height: self.x, self.y = nx, ny - def split(): + def split(self): child = Walker(self.x, self.y) child.dir = self.dir return child From 7cfe55f42cd23c7fbb0d2254abceaaaf837e8ecd Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Mon, 7 Dec 2020 00:24:31 +0100 Subject: [PATCH 07/92] Added a failsafe for cases where the walker population randomly dies out --- dungeonbattle/mapgeneration/randomwalk.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index 60baaba..3aaad70 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -66,6 +66,7 @@ class Generator: # next iteration of the main loop next_walker_pop = [] + failsafe = choice(walkers) for walker in walkers: if grid[walker.y][walker.x] == Tile.WALL: count += 1 @@ -76,6 +77,10 @@ class Generator: if random() > self.params["death_chance"]: next_walker_pop.append(walker) + # we make sure to never kill all walkers + if next_walker_pop == []: + next_walker_pop.append(failsafe) + # we use a second loop for spliting so we're not bothered by cases # like a walker not spliting because we hit the population cap even # though the next one would have died and freed a place From d40a61554ed5cfbeaff61075bd098e64a65abe57 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 01:04:30 +0100 Subject: [PATCH 08/92] Changing the way the tile matrix is declared so that every column is represented by a different list --- dungeonbattle/mapgeneration/randomwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index 3aaad70..de08e9c 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -58,7 +58,7 @@ class Generator: def run(self): width, height = self.params["width"], self.params["height"] walkers = [Walker(width//2, height//2)] - grid = [[Tile.WALL] * width] * height + grid = [[Tile.WALL for _ in range(width)] for _ in range(height)] count = 0 while count < self.params["fill"] * width*height: # because we can't add or remove walkers while looping over the pop From 021731b740e57633654add07b0a7fc56b8f2db40 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 01:09:49 +0100 Subject: [PATCH 09/92] Switching up the tiles used during generation to the correct ones --- dungeonbattle/mapgeneration/randomwalk.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index de08e9c..c9234f9 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -58,7 +58,7 @@ class Generator: def run(self): width, height = self.params["width"], self.params["height"] walkers = [Walker(width//2, height//2)] - grid = [[Tile.WALL for _ in range(width)] for _ in range(height)] + grid = [[Tile.EMPTY for _ in range(width)] for _ in range(height)] count = 0 while count < self.params["fill"] * width*height: # because we can't add or remove walkers while looping over the pop @@ -68,9 +68,9 @@ class Generator: failsafe = choice(walkers) for walker in walkers: - if grid[walker.y][walker.x] == Tile.WALL: + if grid[walker.y][walker.x] == Tile.EMPTY: count += 1 - grid[walker.y][walker.x] = Tile.EMPTY + grid[walker.y][walker.x] = Tile.FLOOR if random() < self.params["turn_chance"]: walker.random_turn() walker.move_in_bounds(width, height) @@ -91,8 +91,9 @@ class Generator: next_walker_pop.append(walker.split()) walkers = next_walker_pop + start_x, start_y = randint(0, width), randint(0, height) - while grid[start_y][start_x] != Tile.EMPTY: + while grid[start_y][start_x] != Tile.FLOOR: start_x, start_y = randint(0, width), randint(0, height) return Map(width, height, grid, start_x, start_y) From 302017222d16aad3280d1e89c3a1bcbdfc86c365 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 01:11:07 +0100 Subject: [PATCH 10/92] Fixing the sampling of the starting position that caused out of bounds error --- dungeonbattle/mapgeneration/randomwalk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index c9234f9..e93a4e6 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -92,8 +92,8 @@ class Generator: walkers = next_walker_pop - start_x, start_y = randint(0, width), randint(0, height) + start_x, start_y = randint(0, width-1), randint(0, height-1) while grid[start_y][start_x] != Tile.FLOOR: - start_x, start_y = randint(0, width), randint(0, height) + start_x, start_y = randint(0, width-1), randint(0, height-1) return Map(width, height, grid, start_x, start_y) From 45120d0c2b48661b4658a1b43d0c95fed4df42db Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 01:13:00 +0100 Subject: [PATCH 11/92] Integrating procedural generation into the game --- dungeonbattle/game.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 46dd0a5..f8a79e1 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -6,6 +6,7 @@ from .enums import GameMode, KeyValues, DisplayActions from .interfaces import Map from .settings import Settings from . import menus +from .mapgeneration import randomwalk from typing import Callable @@ -31,8 +32,7 @@ class Game: """ Create a new game on the screen. """ - # TODO generate a new map procedurally - self.map = Map.load("resources/example_map_2.txt") + self.map = randomwalk.Generator().run() self.player = Player() self.map.add_entity(self.player) self.player.move(self.map.start_y, self.map.start_x) From 29798c135e7a411f111908df74ab853bc9a2ee5d Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 01:24:20 +0100 Subject: [PATCH 12/92] Syntax change for the failsafe --- dungeonbattle/mapgeneration/randomwalk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dungeonbattle/mapgeneration/randomwalk.py b/dungeonbattle/mapgeneration/randomwalk.py index e93a4e6..e4a95c8 100644 --- a/dungeonbattle/mapgeneration/randomwalk.py +++ b/dungeonbattle/mapgeneration/randomwalk.py @@ -66,7 +66,6 @@ class Generator: # next iteration of the main loop next_walker_pop = [] - failsafe = choice(walkers) for walker in walkers: if grid[walker.y][walker.x] == Tile.EMPTY: count += 1 @@ -79,7 +78,7 @@ class Generator: # we make sure to never kill all walkers if next_walker_pop == []: - next_walker_pop.append(failsafe) + next_walker_pop.append(choice(walkers)) # we use a second loop for spliting so we're not bothered by cases # like a walker not spliting because we hit the population cap even From 8751120fe1f9be5275ec08c6a74bb862a89ab5d8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 02:17:00 +0100 Subject: [PATCH 13/92] Merge master into map_generation, there were some commit behind --- squirrelbattle/game.py | 4 ++-- squirrelbattle/mapgeneration/randomwalk.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 6d9e9e7..3f94fd7 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -11,6 +11,7 @@ import sys from .entities.player import Player from .enums import GameMode, KeyValues, DisplayActions from .interfaces import Map, Logs +from .mapgeneration import randomwalk from .resources import ResourceManager from .settings import Settings from . import menus @@ -47,8 +48,7 @@ class Game: """ Create a new game on the screen. """ - # TODO generate a new map procedurally - self.map = Map.load(ResourceManager.get_asset_path("example_map.txt")) + self.map = randomwalk.Generator().run() self.map.logs = self.logs self.logs.clear() self.player = Player() diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index e4a95c8..ef0997d 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -1,6 +1,6 @@ from enum import auto, Enum from random import choice, random, randint -from dungeonbattle.interfaces import Map, Tile +from ..interfaces import Map, Tile DEFAULT_PARAMS = {"split_chance" : .15, From 3c614dcca9e9cbd1c3f71ad8832212cefcaeb5c7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 02:19:59 +0100 Subject: [PATCH 14/92] Linting --- squirrelbattle/mapgeneration/randomwalk.py | 52 ++++++++++++---------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index ef0997d..3595098 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -1,15 +1,22 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from enum import auto, Enum from random import choice, random, randint +from typing import Tuple + from ..interfaces import Map, Tile -DEFAULT_PARAMS = {"split_chance" : .15, - "turn_chance" : .5, - "death_chance" : .1, - "max_walkers" : 15, - "width" : 100, - "height" : 100, - "fill" : .4} +DEFAULT_PARAMS = { + "split_chance": .15, + "turn_chance": .5, + "death_chance": .1, + "max_walkers": 15, + "width": 100, + "height": 100, + "fill": .4, +} class Directions(Enum): @@ -20,16 +27,15 @@ class Directions(Enum): class Walker: - - def __init__(self, x, y): + def __init__(self, x: int, y: int): self.x = x self.y = y self.dir = choice(list(Directions)) - def random_turn(self): + def random_turn(self) -> None: self.dir = choice(list(Directions)) - def next_pos(self): + def next_pos(self) -> Tuple[int, int]: if self.dir == Directions.up: return self.x, self.y + 1 elif self.dir == Directions.down: @@ -39,28 +45,29 @@ class Walker: elif self.dir == Directions.left: return self.x - 1, self.y - def move_in_bounds(self, width, height): + def move_in_bounds(self, width: int, height: int) -> None: nx, ny = self.next_pos() if 0 < nx < width and 0 < ny < height: self.x, self.y = nx, ny - def split(self): + def split(self) -> "Walker": child = Walker(self.x, self.y) child.dir = self.dir return child class Generator: - - def __init__(self, params = DEFAULT_PARAMS): + def __init__(self, params: dict = None): + if params is None: + params = DEFAULT_PARAMS self.params = params - def run(self): + def run(self) -> Map: width, height = self.params["width"], self.params["height"] - walkers = [Walker(width//2, height//2)] + walkers = [Walker(width // 2, height // 2)] grid = [[Tile.EMPTY for _ in range(width)] for _ in range(height)] count = 0 - while count < self.params["fill"] * width*height: + while count < self.params["fill"] * width * height: # because we can't add or remove walkers while looping over the pop # we need lists to keep track of what will be the walkers for the # next iteration of the main loop @@ -77,10 +84,10 @@ class Generator: next_walker_pop.append(walker) # we make sure to never kill all walkers - if next_walker_pop == []: + if not next_walker_pop: next_walker_pop.append(choice(walkers)) - # we use a second loop for spliting so we're not bothered by cases + # we use a second loop for spliting so we're not bothered by cases # like a walker not spliting because we hit the population cap even # though the next one would have died and freed a place # not a big if it happened though @@ -90,9 +97,8 @@ class Generator: next_walker_pop.append(walker.split()) walkers = next_walker_pop - - start_x, start_y = randint(0, width-1), randint(0, height-1) + start_x, start_y = randint(0, width - 1), randint(0, height - 1) while grid[start_y][start_x] != Tile.FLOOR: - start_x, start_y = randint(0, width-1), randint(0, height-1) + start_x, start_y = randint(0, width - 1), randint(0, height - 1) return Map(width, height, grid, start_x, start_y) From 7fb743eb72958b4686b079846541819a4471163e Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 17:02:22 +0100 Subject: [PATCH 15/92] Switching up start_x and start_y so the player spawn is correctly set --- squirrelbattle/mapgeneration/randomwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 3595098..5e5dbff 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -101,4 +101,4 @@ class Generator: while grid[start_y][start_x] != Tile.FLOOR: start_x, start_y = randint(0, width - 1), randint(0, height - 1) - return Map(width, height, grid, start_x, start_y) + return Map(width, height, grid, start_y, start_x) From 3a8549cfcc867f5216a0dcfbcf5efb454bede17c Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 17:09:27 +0100 Subject: [PATCH 16/92] Added a method to interfaces.Map to get the neighbours of a given tile --- squirrelbattle/interfaces.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 3567ea0..50ebb2e 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -5,6 +5,7 @@ from enum import Enum, auto from math import sqrt from random import choice, randint from typing import List, Optional +from itertools import product from .display.texturepack import TexturePack from .translations import gettext as _ @@ -180,6 +181,13 @@ class Map: for entisave in d["entities"]: self.add_entity(dictclasses[entisave["type"]](**entisave)) + def large_neighbourhood(self, y, x): + neighbours = [] + for dy, dx in product([-1, 0, 1], [-1, 0, 1]): + if 0 < y+dy < self.height and 0 < x+dx < self.width: + neighbours.append([y+dy, x+dx]) + return neighbours + class Tile(Enum): """ From 6a4d13c7264758f24490ade84c2eafb6d2d1a7a6 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 17:09:59 +0100 Subject: [PATCH 17/92] Walls now generate around the floor --- squirrelbattle/mapgeneration/randomwalk.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 5e5dbff..ca7814b 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -16,6 +16,7 @@ DEFAULT_PARAMS = { "width": 100, "height": 100, "fill": .4, + "no_lone_walls": False, } @@ -101,4 +102,15 @@ class Generator: while grid[start_y][start_x] != Tile.FLOOR: start_x, start_y = randint(0, width - 1), randint(0, height - 1) - return Map(width, height, grid, start_y, start_x) + result = Map(width, height, grid, start_y, start_x) + + # post-processing: add walls + for x in range(width): + for y in range(height): + c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.neighbours_large(y, x)]) + if c == 4 and self.params["no_lone_walls"]: + result.tiles[y][x] = Tile.FLOOR + elif c > 0: + result.tiles[y][x] = Tile.WALL + + return result From 757a460a44082f16849ca897b4cd3573945bac3e Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 17:13:12 +0100 Subject: [PATCH 18/92] Fix typo --- squirrelbattle/mapgeneration/randomwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index ca7814b..3ef2b2d 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -107,7 +107,7 @@ class Generator: # post-processing: add walls for x in range(width): for y in range(height): - c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.neighbours_large(y, x)]) + c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.large_neighbourhood(y, x)]) if c == 4 and self.params["no_lone_walls"]: result.tiles[y][x] = Tile.FLOOR elif c > 0: From c8b07b3bf59175e2285fe57cb487b254cfd55f9a Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 17:17:11 +0100 Subject: [PATCH 19/92] Only empty tiles should be changed to walls, obviously... --- squirrelbattle/mapgeneration/randomwalk.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 3ef2b2d..b913d1c 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -107,10 +107,11 @@ class Generator: # post-processing: add walls for x in range(width): for y in range(height): - c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.large_neighbourhood(y, x)]) - if c == 4 and self.params["no_lone_walls"]: - result.tiles[y][x] = Tile.FLOOR - elif c > 0: - result.tiles[y][x] = Tile.WALL + if grid[y][x] == Tile.EMPTY: + c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.large_neighbourhood(y, x)]) + if c == 4 and self.params["no_lone_walls"]: + result.tiles[y][x] = Tile.FLOOR + elif c > 0: + result.tiles[y][x] = Tile.WALL return result From d3c14a48ee4168c709c8941e92319477550abed3 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 17:46:49 +0100 Subject: [PATCH 20/92] Add docstring for Map.large_neighbourhood --- squirrelbattle/interfaces.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 50ebb2e..9d13046 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -182,6 +182,10 @@ class Map: self.add_entity(dictclasses[entisave["type"]](**entisave)) def large_neighbourhood(self, y, x): + """ + Returns up to 8 nearby coordinates, in a 3x3 square around the input coordinate. + Does not return coordinates if they are out of bounds. + """ neighbours = [] for dy, dx in product([-1, 0, 1], [-1, 0, 1]): if 0 < y+dy < self.height and 0 < x+dx < self.width: From 7667079aa3c026d8fb067f8453cda69bc7c3f7ef Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 18:33:16 +0100 Subject: [PATCH 21/92] Changed Map.large_neighbourhood so we can also request only immediate neighbours, ignoring diagonals --- squirrelbattle/interfaces.py | 11 ++++++----- squirrelbattle/mapgeneration/randomwalk.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 9d13046..41b11f0 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -180,14 +180,15 @@ class Map: dictclasses = Entity.get_all_entity_classes_in_a_dict() for entisave in d["entities"]: self.add_entity(dictclasses[entisave["type"]](**entisave)) - - def large_neighbourhood(self, y, x): + def neighbourhood(self, y, x, large=False): """ - Returns up to 8 nearby coordinates, in a 3x3 square around the input coordinate. - Does not return coordinates if they are out of bounds. + Returns up to 8 nearby coordinates, in a 3x3 square around the input coordinate if large is + set to True, or in a 5-square cross by default. Does not return coordinates if they are out + of bounds. """ neighbours = [] - for dy, dx in product([-1, 0, 1], [-1, 0, 1]): + dyxs = product([-1, 0, 1], [-1, 0, 1]) if large else [[0, -1], [0, 1], [-1, 0], [1, 0]] + for dy, dx in dyxs: if 0 < y+dy < self.height and 0 < x+dx < self.width: neighbours.append([y+dy, x+dx]) return neighbours diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index b913d1c..bd3a20d 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -108,7 +108,7 @@ class Generator: for x in range(width): for y in range(height): if grid[y][x] == Tile.EMPTY: - c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.large_neighbourhood(y, x)]) + c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.neighbourhood(y, x, large=True]) if c == 4 and self.params["no_lone_walls"]: result.tiles[y][x] = Tile.FLOOR elif c > 0: From 18ca083ba213142416f7f7d28458f8c0ee3144b2 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 18:59:07 +0100 Subject: [PATCH 22/92] Added a connexity test --- squirrelbattle/tests/mapgeneration_test.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 squirrelbattle/tests/mapgeneration_test.py diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py new file mode 100644 index 0000000..abaed1b --- /dev/null +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -0,0 +1,29 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + +import unittest + +from squirrelbattle.interfaces import Map, Tile +from squirrelbattle.mapgeneration import randomwalk + + +class TestRandomWalk(unittest.TestCase): + def setUp(self) -> None: + self.generator = randomwalk.Generator() + + def test_starting(self) -> None: + """ + Create a map and check that the whole map is accessible from the starting position using a + depth-first search + """ + m = self.generator.run() + self.assertTrue(m.tiles[m.start_y][m.start_x].can_walk()) + + #DFS + queue = m.neighbourhood(m.start_y, m.start_x) + while queue != []: + y, x = queue.pop() + if m.tiles[y][x].can_walk(): + m.tiles[y][x] = Tile.WALL + queue += m.neighbourhood(y, x) + self.assertFalse(any([any([t.can_walk() for t in l]) for l in m.tiles])) From deb52d73502eb481ffd208ea7c0c26d2e90f05d5 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 19:05:26 +0100 Subject: [PATCH 23/92] Adding a missing parenthesis --- squirrelbattle/mapgeneration/randomwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index bd3a20d..b3287ea 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -108,7 +108,7 @@ class Generator: for x in range(width): for y in range(height): if grid[y][x] == Tile.EMPTY: - c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.neighbourhood(y, x, large=True]) + c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.neighbourhood(y, x, large=True)]) if c == 4 and self.params["no_lone_walls"]: result.tiles[y][x] = Tile.FLOOR elif c > 0: From fe9dfdf242f32cd02b2041415306f11ae81d948d Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 19:13:15 +0100 Subject: [PATCH 24/92] Syntax change in randomwalk.Generator.__init__ --- squirrelbattle/mapgeneration/randomwalk.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index b3287ea..32ec0d6 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -58,9 +58,7 @@ class Walker: class Generator: - def __init__(self, params: dict = None): - if params is None: - params = DEFAULT_PARAMS + def __init__(self, params: dict = DEFAULT_PARAMS): self.params = params def run(self) -> Map: From 3d7667573e158c454031e9e6633f898151bf49ca Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 11 Dec 2020 19:14:28 +0100 Subject: [PATCH 25/92] Add testing for the no_lone_walls option --- squirrelbattle/tests/mapgeneration_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index abaed1b..ed0951c 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -9,7 +9,10 @@ from squirrelbattle.mapgeneration import randomwalk class TestRandomWalk(unittest.TestCase): def setUp(self) -> None: - self.generator = randomwalk.Generator() + #we set no_lone_walls to true for 100% coverage + params = randomwalk.DEFAULT_PARAMS + params["no_lone_walls"] = True + self.generator = randomwalk.Generator(params = params) def test_starting(self) -> None: """ From 895abe88ad3ec947a9b6a0911f650ab774135fa9 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 19:14:25 +0100 Subject: [PATCH 26/92] Ensure that the neighboorhood is walkable in movement tests --- squirrelbattle/tests/game_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index bc3ce12..8203299 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -12,6 +12,7 @@ from ..entities.items import Bomb, Heart, Sword from ..entities.player import Player from ..enums import DisplayActions from ..game import Game, KeyValues, GameMode +from ..interfaces import Tile from ..menus import MainMenuValues from ..resources import ResourceManager from ..settings import Settings @@ -204,6 +205,12 @@ class TestGame(unittest.TestCase): self.game.map.remove_entity(entity) y, x = self.game.player.y, self.game.player.x + + # Ensure that the neighborhood is walkable + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + self.game.map.tiles[y + dy][x + dx] = Tile.FLOOR + self.game.handle_key_pressed(KeyValues.DOWN) new_y, new_x = self.game.player.y, self.game.player.x self.assertEqual(new_y, y + 1) From 209bde5b5c23bc6c278e3cb1d4bf49bdf8aa5b1c Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 19:21:02 +0100 Subject: [PATCH 27/92] Fix sunflowers and merchants since the position of the player is no longer fixed --- squirrelbattle/tests/game_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 8203299..b21a753 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -473,7 +473,7 @@ class TestGame(unittest.TestCase): self.game.state = GameMode.PLAY sunflower = Sunflower() - sunflower.move(2, 6) + sunflower.move(self.game.player.y + 1, self.game.player.x) self.game.map.add_entity(sunflower) # Does nothing @@ -504,15 +504,15 @@ class TestGame(unittest.TestCase): Sunflower.dialogue_option) # Test all directions to detect the friendly entity - self.game.player.move(3, 6) + self.game.player.move(sunflower.y + 1, sunflower.x) self.game.handle_key_pressed(KeyValues.CHAT) self.game.handle_key_pressed(KeyValues.UP) self.assertEqual(len(self.game.logs.messages), 3) - self.game.player.move(2, 7) + self.game.player.move(sunflower.y, sunflower.x + 1) self.game.handle_key_pressed(KeyValues.CHAT) self.game.handle_key_pressed(KeyValues.LEFT) self.assertEqual(len(self.game.logs.messages), 4) - self.game.player.move(2, 5) + self.game.player.move(sunflower.y, sunflower.x - 1) self.game.handle_key_pressed(KeyValues.CHAT) self.game.handle_key_pressed(KeyValues.RIGHT) self.assertEqual(len(self.game.logs.messages), 5) @@ -524,7 +524,7 @@ class TestGame(unittest.TestCase): self.game.state = GameMode.PLAY merchant = Merchant() - merchant.move(2, 6) + merchant.move(self.game.player.y + 1, self.game.player.x) self.game.map.add_entity(merchant) # Does nothing From fb926f8c844c2e08ba8a368c427b6b6ffc5aaa34 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 11 Dec 2020 19:27:57 +0100 Subject: [PATCH 28/92] Always use predefined map in game unit tests --- squirrelbattle/interfaces.py | 2 ++ squirrelbattle/tests/game_test.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 3f72682..ef74614 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -54,6 +54,8 @@ class Map: self.height = height self.start_y = start_y self.start_x = start_x + self.currenty = start_y + self.currentx = start_x self.tiles = tiles self.entities = [] self.logs = Logs() diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index b21a753..e149444 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -12,7 +12,7 @@ from ..entities.items import Bomb, Heart, Sword from ..entities.player import Player from ..enums import DisplayActions from ..game import Game, KeyValues, GameMode -from ..interfaces import Tile +from ..interfaces import Tile, Map from ..menus import MainMenuValues from ..resources import ResourceManager from ..settings import Settings @@ -26,6 +26,10 @@ class TestGame(unittest.TestCase): """ self.game = Game() self.game.new_game() + self.game.map = Map.load(ResourceManager.get_asset_path( + "example_map.txt")) + self.game.player.move(self.game.map.start_y, self.game.map.start_x) + self.game.map.add_entity(self.game.player) self.game.logs.add_message("Hello World !") display = DisplayManager(None, self.game) self.game.display_actions = display.handle_display_action From 5fbb9181320e3bf742a706639a303da2d8e6fc22 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 18 Dec 2020 17:05:50 +0100 Subject: [PATCH 29/92] Add walls even to map borders --- squirrelbattle/mapgeneration/randomwalk.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 32ec0d6..3acb58a 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -111,5 +111,13 @@ class Generator: result.tiles[y][x] = Tile.FLOOR elif c > 0: result.tiles[y][x] = Tile.WALL + for x in range(width): + for y in [0, height-1]: + if grid[y][x] = Tile.FLOOR: + grid[y][x] = Tile.WALL + for y in range(height): + for y in [0, width-1]: + if grid[y][x] = Tile.FLOOR: + grid[y][x] = Tile.WALL return result From ba3d979f9c6ca72ccefe98d12a11e53bcfb7077c Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 18 Dec 2020 18:10:52 +0100 Subject: [PATCH 30/92] Fix syntax error --- squirrelbattle/mapgeneration/randomwalk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 3acb58a..d4669d4 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -113,11 +113,11 @@ class Generator: result.tiles[y][x] = Tile.WALL for x in range(width): for y in [0, height-1]: - if grid[y][x] = Tile.FLOOR: + if grid[y][x] == Tile.FLOOR: grid[y][x] = Tile.WALL for y in range(height): for y in [0, width-1]: - if grid[y][x] = Tile.FLOOR: + if grid[y][x] == Tile.FLOOR: grid[y][x] = Tile.WALL return result From f5e5e365d47f0e49f98f02f5945c25487a24abe1 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 18 Dec 2020 20:02:37 +0100 Subject: [PATCH 31/92] Starting the implementation of the new map generator --- squirrelbattle/mapgeneration/broguelike.py | 51 ++++++++++++++++++++++ squirrelbattle/mapgeneration/randomwalk.py | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 squirrelbattle/mapgeneration/broguelike.py diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py new file mode 100644 index 0000000..7572b05 --- /dev/null +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -0,0 +1,51 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + +from enum import auto, Enum +from random import choice, random, randint + +from ..interfaces import Map, Tile + + +DEFAULT_PARAMS = { + "width" : 80, + "height" : 40, + "tries" : 600, + "max_rooms" : 99, + "cross_room" : 1, + "corridor" : .8, + "min_v_corridor" : + "max_v_corridor" : + "min_h_corridor" : + "max_h_corridor" : + "large_circular_room" : .10, + "circular_holes" : .5, +} + + +class Generator: + def __init__(self, params: dict = DEFAULT_PARAMS): + self.params = params + + def createCircularRoom(self): + if random() < self.params["large_circular_room"]: + r = randint(5, 10)**2 + else: + r = randint(2, 4)**2 + + room = [] + height = 2*r+2+self.params["max_h_corridor"] + width = 2*r+2+self.params["max_v_corridor"] + make_hole = random() < self.params["circular_holes"] + if make_hole: + r2 = randint(3, r-3) + for i in range(height): + room.append([]) + d = (i-height//2)**2 + for j in range(width): + if d + (j-width//2)**2 < r**2 and \ + (not(make_hole) or d + (j-width//2)**2 >= r2**2): + room[-1].append(Tile.FLOOR) + else: + room[-1].append(Tile.EMPTY) + return room diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index d4669d4..7cac8ff 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -116,7 +116,7 @@ class Generator: if grid[y][x] == Tile.FLOOR: grid[y][x] = Tile.WALL for y in range(height): - for y in [0, width-1]: + for x in [0, width-1]: if grid[y][x] == Tile.FLOOR: grid[y][x] = Tile.WALL From 9fb366aaab6c599fa03b7f7ccbe8ca24de951941 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Thu, 7 Jan 2021 05:02:49 +0100 Subject: [PATCH 32/92] Make name follow style convention --- squirrelbattle/mapgeneration/broguelike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 7572b05..b5ea122 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -27,7 +27,7 @@ class Generator: def __init__(self, params: dict = DEFAULT_PARAMS): self.params = params - def createCircularRoom(self): + def create_circular_room(self): if random() < self.params["large_circular_room"]: r = randint(5, 10)**2 else: From 5579f5d7913c22b5aa0f5f9c8403ae0fbb44ea59 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Thu, 7 Jan 2021 07:06:08 +0100 Subject: [PATCH 33/92] Room now can now generate with a corridor; implemenent door placement finding --- squirrelbattle/mapgeneration/broguelike.py | 79 ++++++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index b5ea122..6b394d5 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from enum import auto, Enum -from random import choice, random, randint +from random import choice, random, randint, shuffle from ..interfaces import Map, Tile @@ -12,12 +12,13 @@ DEFAULT_PARAMS = { "height" : 40, "tries" : 600, "max_rooms" : 99, + "max_room_tries" : 15, "cross_room" : 1, - "corridor" : .8, - "min_v_corridor" : - "max_v_corridor" : - "min_h_corridor" : - "max_h_corridor" : + "corridor_chance" : .8, + "min_v_corr" : 2, + "max_v_corr" : 6, + "min_h_corr" : 4, + "max_h_corr" : 12, "large_circular_room" : .10, "circular_holes" : .5, } @@ -27,6 +28,49 @@ class Generator: def __init__(self, params: dict = DEFAULT_PARAMS): self.params = params + def corr_meta_info(self): + if random() < self.params["corridor_chance"]: + h_sup = randint(self.params["min_h_corr"], \ + self.params["max_h_corr"]) if random() < .5 else 0 + w_sup = 0 if h_sup else randint(self.params["min_w_corr"], \ + self.params["max_w_coor"]) + h_off = h_sup if random() < .5 else 0 + w_off = w_sup if random() < .5 else 0 + return h_sup, w_sup, h_off, w_off + return 0, 0, 0, 0 + + def attach_door(self, room, h_sup, w_sup, h_off, w_off): + l = h_sup + w_sup + dy, dx = 0, 0 + if l > 0: + if h_sup: + dy = -1 if h_off else 1 + else: + dx = -1 if w_off else 1 + else: + if random() < .5: + dy = -1 if random() < .5 else 1 + else: + dx = -1 if random() < .5 else 1 + + yxs = [i for i in range(len(room) * len(room[0]))] + shuffle(xys) + for POS in yxs: + y, x = POS // len(room), POS % len(room) + if room[y][x] == Tile.EMPTY: + if room[y-dy][x-dx] == Tile.FLOOR: + build_here = True + for i in range(l): + if room[y+i*dy][x+i*dx] != Tile.EMPTY: + build_here = False + break + if build_here: + for i in range(l): + room[y+i*dy][x+i*dx] == Tile.FLOOR + break + return y+l*dy, x+l*dx + + def create_circular_room(self): if random() < self.params["large_circular_room"]: r = randint(5, 10)**2 @@ -34,18 +78,25 @@ class Generator: r = randint(2, 4)**2 room = [] - height = 2*r+2+self.params["max_h_corridor"] - width = 2*r+2+self.params["max_v_corridor"] + + h_sup, w_sup, h_off, w_off = self.corr_meta_info() + + height = 2*r+2 + width = 2*r+2 make_hole = random() < self.params["circular_holes"] if make_hole: r2 = randint(3, r-3) - for i in range(height): + for i in range(height+h_sup): room.append([]) - d = (i-height//2)**2 - for j in range(width): - if d + (j-width//2)**2 < r**2 and \ - (not(make_hole) or d + (j-width//2)**2 >= r2**2): + d = (i-h_off-height//2)**2 + for j in range(width+w_sup): + if d + (j-w_off-width//2)**2 < r**2 and \ + (not(make_hole) or d + (j-w_off-width//2)**2 >= r2**2): room[-1].append(Tile.FLOOR) else: room[-1].append(Tile.EMPTY) - return room + + door_y, door_x = self.attach_doors(room, h_sup, w_sup, h_off, w_off) + + return room, doory, doorx + From bb3422f7d85c48d2414241ea25a3c576e6345b01 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 03:19:59 +0100 Subject: [PATCH 34/92] Add main generation loop --- squirrelbattle/mapgeneration/broguelike.py | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 6b394d5..db81065 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -100,3 +100,28 @@ class Generator: return room, doory, doorx + def run(self): + height, width = self.params["height"], self.params["width"] + level = [[Tile.EMPTY for i in range(width)] for j in range(height)] + + # the starting room must have no corridor + mem, self.params["corridor"] = self.params["corridor"], 0 + starting_room, _, _, _, _ = self.create_random_room() + self.place_room(level, height//2, width//2, 0, 0, starting_room) + self.params["corridor"] = mem + + tries, rooms_built = 0, 0 + while tries < self.params["tries"] and rooms_built < self.params["max_rooms"]: + + room, door_y, door_x, dy, dx = self.create_random_room() + positions = [i for i in range()] + shuffle(positions) + for pos in positions: + y, x = pos // height, pos % width + if self.room_fits(level, y, x, room, door_y, door_x, dy, dx): + self.place_room(level, y, x, door_y, door_x, room) + + # post-processing + self.place_walls(level) + + return level From 5cbf15bef5ffc698a33d44d7fccf31344847660f Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 03:37:10 +0100 Subject: [PATCH 35/92] Return value of Generator.run should be a Map --- squirrelbattle/mapgeneration/broguelike.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index db81065..f60d423 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -110,6 +110,12 @@ class Generator: self.place_room(level, height//2, width//2, 0, 0, starting_room) self.params["corridor"] = mem + # find a starting position + sy, sx = randint(0, height-1), randint(0, width-1) + while level[sy][sx] != Tile.FLOOR: + sy, sx = randint(0, height-1), randint(0, width-1) + + # now we loop until we've tried enough, or we've added enough rooms tries, rooms_built = 0, 0 while tries < self.params["tries"] and rooms_built < self.params["max_rooms"]: @@ -124,4 +130,4 @@ class Generator: # post-processing self.place_walls(level) - return level + return Map(width, height, level, sy, sx) From ddbd0299a09110c6ed247cdac2be8dd247d0866b Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 03:38:37 +0100 Subject: [PATCH 36/92] Implement method room_fits --- squirrelbattle/mapgeneration/broguelike.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index f60d423..0a92180 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -28,6 +28,20 @@ class Generator: def __init__(self, params: dict = DEFAULT_PARAMS): self.params = params + @staticmethod + def room_fits(level, y, x, room, door_y, door_x, dy, dx): + if level[y][x] != Tile.EMPTY or level[y-dy][x-dx] != Tile.FLOOR: + return False + lh, lw = len(level), len(level[0]) + rh, rw = len(room), len(room[0]) + for ry in range(rh): + for rx in range(rw): + if room[y][x] == Tile.FLOOR: + ly, lx = ry - door_y, rx - door_x + if not(0 <= ly <= rh and 0 <= lx <= rw) or \ + level[ly][lx] == Tile.FLOOR: + return False + return True def corr_meta_info(self): if random() < self.params["corridor_chance"]: h_sup = randint(self.params["min_h_corr"], \ From 42f0c195aa373b7a80f7206cf18c8201a0154dff Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 03:43:20 +0100 Subject: [PATCH 37/92] Add prototype for create_random_room; change return value of attach_doors and create_circular_room so we have info on door direction; minor syntax change --- squirrelbattle/mapgeneration/broguelike.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 0a92180..9dad093 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -69,8 +69,8 @@ class Generator: yxs = [i for i in range(len(room) * len(room[0]))] shuffle(xys) - for POS in yxs: - y, x = POS // len(room), POS % len(room) + for pos in yxs: + y, x = pos // len(room), pos % len(room) if room[y][x] == Tile.EMPTY: if room[y-dy][x-dx] == Tile.FLOOR: build_here = True @@ -82,7 +82,7 @@ class Generator: for i in range(l): room[y+i*dy][x+i*dx] == Tile.FLOOR break - return y+l*dy, x+l*dx + return y+l*dy, x+l*dx, dy, dx def create_circular_room(self): @@ -110,10 +110,13 @@ class Generator: else: room[-1].append(Tile.EMPTY) - door_y, door_x = self.attach_doors(room, h_sup, w_sup, h_off, w_off) + door_y, door_x, dy, dx = self.attach_doors(room, h_sup, w_sup, h_off, w_off) - return room, doory, doorx + return room, doory, doorx, dy, dx + def create_random_room(self): + return create_circular_room(self) + def run(self): height, width = self.params["height"], self.params["width"] level = [[Tile.EMPTY for i in range(width)] for j in range(height)] From 3229eb8ea71dacaefc5a4c19f64a8cf8b741c52e Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 03:45:26 +0100 Subject: [PATCH 38/92] Implement place_room method --- squirrelbattle/mapgeneration/broguelike.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 9dad093..1a6f70c 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -42,6 +42,16 @@ class Generator: level[ly][lx] == Tile.FLOOR: return False return True + + @staticmethod + def place_room(level, y, x, door_y, door_x, room): + rh, rw = len(room), len(room[0]) + # maybe place Tile.DOOR here ? + level[door_y][door_x] = Tile.FLOOR + for ry in range(rh): + for rx in range(rw): + if room[y][x] == Tile.FLOOR: + level[y-door_y][y-door_x] = Tile.FLOOR def corr_meta_info(self): if random() < self.params["corridor_chance"]: h_sup = randint(self.params["min_h_corr"], \ From ffa7641b215b5ec47650a81b6432bde850509f14 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 04:36:57 +0100 Subject: [PATCH 39/92] Made Map.neighbourhood a static method --- squirrelbattle/interfaces.py | 7 +++++-- squirrelbattle/mapgeneration/randomwalk.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index ef74614..2a08abc 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -192,16 +192,19 @@ class Map: dictclasses = Entity.get_all_entity_classes_in_a_dict() for entisave in d["entities"]: self.add_entity(dictclasses[entisave["type"]](**entisave)) - def neighbourhood(self, y, x, large=False): + + @staticmethod + def neighbourhood(grid, y, x, large=False): """ Returns up to 8 nearby coordinates, in a 3x3 square around the input coordinate if large is set to True, or in a 5-square cross by default. Does not return coordinates if they are out of bounds. """ + height, width = len(grid), len(grid[0]) neighbours = [] dyxs = product([-1, 0, 1], [-1, 0, 1]) if large else [[0, -1], [0, 1], [-1, 0], [1, 0]] for dy, dx in dyxs: - if 0 < y+dy < self.height and 0 < x+dx < self.width: + if 0 < y+dy < height and 0 < x+dx < width: neighbours.append([y+dy, x+dx]) return neighbours diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 7cac8ff..913599f 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -106,7 +106,7 @@ class Generator: for x in range(width): for y in range(height): if grid[y][x] == Tile.EMPTY: - c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in result.neighbourhood(y, x, large=True)]) + c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in Map.neighbourhood(grid, y, x, large=True)]) if c == 4 and self.params["no_lone_walls"]: result.tiles[y][x] = Tile.FLOOR elif c > 0: From 6fbc757f1e2b46e7dc4da95400dc26b1fbdeb81e Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 04:43:10 +0100 Subject: [PATCH 40/92] Implement method place_walls --- squirrelbattle/mapgeneration/broguelike.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 1a6f70c..6101ceb 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -52,6 +52,17 @@ class Generator: for rx in range(rw): if room[y][x] == Tile.FLOOR: level[y-door_y][y-door_x] = Tile.FLOOR + + @staticmethod + def place_walls(level): + h, w = len(level), len(level[0]) + for y in range(h): + for x in range(w): + if level[y][x] == Tile.FLOOR: + for dy, dx in Map.neighbourhood(level, y, x): + if level[y+dy][x+dx] == Tile.EMPTY: + level[y+dy][x+dx] = Tile.FLOOR + def corr_meta_info(self): if random() < self.params["corridor_chance"]: h_sup = randint(self.params["min_h_corr"], \ From c6f66d95f2b2f683baef7fd9ce14c744034fc38b Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 04:48:32 +0100 Subject: [PATCH 41/92] Fix typos --- squirrelbattle/mapgeneration/broguelike.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 6101ceb..52ec246 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -136,17 +136,17 @@ class Generator: return room, doory, doorx, dy, dx def create_random_room(self): - return create_circular_room(self) + return self.create_circular_room() def run(self): height, width = self.params["height"], self.params["width"] level = [[Tile.EMPTY for i in range(width)] for j in range(height)] # the starting room must have no corridor - mem, self.params["corridor"] = self.params["corridor"], 0 + mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 starting_room, _, _, _, _ = self.create_random_room() self.place_room(level, height//2, width//2, 0, 0, starting_room) - self.params["corridor"] = mem + self.params["corridor_chance"] = mem # find a starting position sy, sx = randint(0, height-1), randint(0, width-1) From 05ccd0e33976ec691e91ad86c5a36aac4d48353f Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 04:51:20 +0100 Subject: [PATCH 42/92] Circular rooms should not try to generate any holes if their radius isn't large enough --- squirrelbattle/mapgeneration/broguelike.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 52ec246..c208e91 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -108,9 +108,9 @@ class Generator: def create_circular_room(self): if random() < self.params["large_circular_room"]: - r = randint(5, 10)**2 + r = randint(5, 10) else: - r = randint(2, 4)**2 + r = randint(2, 4) room = [] @@ -118,7 +118,7 @@ class Generator: height = 2*r+2 width = 2*r+2 - make_hole = random() < self.params["circular_holes"] + make_hole = r > 6 and random() < self.params["circular_holes"] if make_hole: r2 = randint(3, r-3) for i in range(height+h_sup): From abbad0f352553fdb989b48a96d1bb5a9032f4175 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 05:14:32 +0100 Subject: [PATCH 43/92] Fix formulas in place_room and room_fits --- squirrelbattle/mapgeneration/broguelike.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index c208e91..ea3fe46 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -36,8 +36,8 @@ class Generator: rh, rw = len(room), len(room[0]) for ry in range(rh): for rx in range(rw): - if room[y][x] == Tile.FLOOR: - ly, lx = ry - door_y, rx - door_x + if room[ry][rx] == Tile.FLOOR: + ly, lx = y + ry - door_y, x + rx - door_x if not(0 <= ly <= rh and 0 <= lx <= rw) or \ level[ly][lx] == Tile.FLOOR: return False @@ -50,8 +50,8 @@ class Generator: level[door_y][door_x] = Tile.FLOOR for ry in range(rh): for rx in range(rw): - if room[y][x] == Tile.FLOOR: - level[y-door_y][y-door_x] = Tile.FLOOR + if room[ry][rx] == Tile.FLOOR: + level[y-door_y+ry][y-door_x+rx] = Tile.FLOOR @staticmethod def place_walls(level): From 49e261557cb7aac00f845da6ec81da49810e5f62 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 05:14:46 +0100 Subject: [PATCH 44/92] Fix typos --- squirrelbattle/mapgeneration/broguelike.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index ea3fe46..9d6af48 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -23,7 +23,6 @@ DEFAULT_PARAMS = { "circular_holes" : .5, } - class Generator: def __init__(self, params: dict = DEFAULT_PARAMS): self.params = params @@ -89,7 +88,7 @@ class Generator: dx = -1 if random() < .5 else 1 yxs = [i for i in range(len(room) * len(room[0]))] - shuffle(xys) + shuffle(yxs) for pos in yxs: y, x = pos // len(room), pos % len(room) if room[y][x] == Tile.EMPTY: @@ -131,9 +130,9 @@ class Generator: else: room[-1].append(Tile.EMPTY) - door_y, door_x, dy, dx = self.attach_doors(room, h_sup, w_sup, h_off, w_off) + door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup, h_off, w_off) - return room, doory, doorx, dy, dx + return room, door_y, door_x, dy, dx def create_random_room(self): return self.create_circular_room() From 20cbf546f9c80bcd357d65888b3a9cdd037895e6 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 05:21:31 +0100 Subject: [PATCH 45/92] Correct formulas for random enumeration of a grid --- squirrelbattle/mapgeneration/broguelike.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 9d6af48..72f773c 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -90,7 +90,7 @@ class Generator: yxs = [i for i in range(len(room) * len(room[0]))] shuffle(yxs) for pos in yxs: - y, x = pos // len(room), pos % len(room) + y, x = pos // len(room[0]), pos % len(room[0]) if room[y][x] == Tile.EMPTY: if room[y-dy][x-dx] == Tile.FLOOR: build_here = True @@ -160,7 +160,7 @@ class Generator: positions = [i for i in range()] shuffle(positions) for pos in positions: - y, x = pos // height, pos % width + y, x = pos // width, pos % width if self.room_fits(level, y, x, room, door_y, door_x, dy, dx): self.place_room(level, y, x, door_y, door_x, room) From 8475e5228e603e23f7682460d0ac60247a24198b Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 05:41:16 +0100 Subject: [PATCH 46/92] Large neighbourhood shouldn't return the central cell --- squirrelbattle/interfaces.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 2a08abc..4e5f9ff 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -202,9 +202,13 @@ class Map: """ height, width = len(grid), len(grid[0]) neighbours = [] - dyxs = product([-1, 0, 1], [-1, 0, 1]) if large else [[0, -1], [0, 1], [-1, 0], [1, 0]] + if large: + dyxs = product([-1, 0, 1], [-1, 0, 1]) + dyxs = dyxs[:5] + dyxs[6:] + else: + dyxs = [[0, -1], [0, 1], [-1, 0], [1, 0]] for dy, dx in dyxs: - if 0 < y+dy < height and 0 < x+dx < width: + if 0 <= y+dy < height and 0 <= x+dx < width: neighbours.append([y+dy, x+dx]) return neighbours From c959a9d865949848f0b2f86692fe0aee95a5052a Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 05:42:12 +0100 Subject: [PATCH 47/92] Update tests because Map.neighbourhood became a static method --- squirrelbattle/tests/mapgeneration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index ed0951c..4fc6b28 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -23,7 +23,7 @@ class TestRandomWalk(unittest.TestCase): self.assertTrue(m.tiles[m.start_y][m.start_x].can_walk()) #DFS - queue = m.neighbourhood(m.start_y, m.start_x) + queue = Map.neighbourhood(m.tiles, m.start_y, m.start_x) while queue != []: y, x = queue.pop() if m.tiles[y][x].can_walk(): From 9c252a2bbc135a8d97cf23a4970478640ac5870e Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 06:54:01 +0100 Subject: [PATCH 48/92] Correct out of bounds errors and add missing arguments to range call --- squirrelbattle/mapgeneration/broguelike.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 72f773c..d6da979 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -29,15 +29,17 @@ class Generator: @staticmethod def room_fits(level, y, x, room, door_y, door_x, dy, dx): - if level[y][x] != Tile.EMPTY or level[y-dy][x-dx] != Tile.FLOOR: - return False lh, lw = len(level), len(level[0]) rh, rw = len(room), len(room[0]) + if not(0 < y+dy < lh and 0 < x+dx < lw): + return False + if level[y][x] != Tile.EMPTY or level[y+dy][x+dx] != Tile.FLOOR: + return False for ry in range(rh): for rx in range(rw): if room[ry][rx] == Tile.FLOOR: ly, lx = y + ry - door_y, x + rx - door_x - if not(0 <= ly <= rh and 0 <= lx <= rw) or \ + if not(0 <= ly < lh and 0 <= lx < lw) or \ level[ly][lx] == Tile.FLOOR: return False return True From d362bdc949179c1d6afe68feb9bf44eeb0199cd4 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 06:58:02 +0100 Subject: [PATCH 49/92] Fix place_room and add missing argument --- squirrelbattle/mapgeneration/broguelike.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index d6da979..40529e5 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -45,14 +45,14 @@ class Generator: return True @staticmethod - def place_room(level, y, x, door_y, door_x, room): + def place_room(level, y, x, room, door_y, door_x): rh, rw = len(room), len(room[0]) # maybe place Tile.DOOR here ? - level[door_y][door_x] = Tile.FLOOR + level[y][x] = Tile.FLOOR for ry in range(rh): for rx in range(rw): if room[ry][rx] == Tile.FLOOR: - level[y-door_y+ry][y-door_x+rx] = Tile.FLOOR + level[y-door_y+ry][x-door_x+rx] = Tile.FLOOR @staticmethod def place_walls(level): @@ -146,9 +146,9 @@ class Generator: # the starting room must have no corridor mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 starting_room, _, _, _, _ = self.create_random_room() - self.place_room(level, height//2, width//2, 0, 0, starting_room) + self.place_room(level, height//2, width//2, starting_room, 0, 0) self.params["corridor_chance"] = mem - + # find a starting position sy, sx = randint(0, height-1), randint(0, width-1) while level[sy][sx] != Tile.FLOOR: @@ -159,13 +159,13 @@ class Generator: while tries < self.params["tries"] and rooms_built < self.params["max_rooms"]: room, door_y, door_x, dy, dx = self.create_random_room() - positions = [i for i in range()] + positions = [i for i in range(height * width)] shuffle(positions) for pos in positions: y, x = pos // width, pos % width if self.room_fits(level, y, x, room, door_y, door_x, dy, dx): - self.place_room(level, y, x, door_y, door_x, room) - + self.place_room(level, y, x, room, door_y, door_x) + # post-processing self.place_walls(level) From b0ac580677a5f46ec1b61412f3025d9a5d8ab283 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 07:03:49 +0100 Subject: [PATCH 50/92] Fix place_walls, that placed floors instead ... --- squirrelbattle/mapgeneration/broguelike.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 40529e5..dff9a7c 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -60,9 +60,9 @@ class Generator: for y in range(h): for x in range(w): if level[y][x] == Tile.FLOOR: - for dy, dx in Map.neighbourhood(level, y, x): - if level[y+dy][x+dx] == Tile.EMPTY: - level[y+dy][x+dx] = Tile.FLOOR + for ny, nx in Map.neighbourhood(level, y, x): + if level[ny][nx] == Tile.EMPTY: + level[ny][nx] = Tile.WALL def corr_meta_info(self): if random() < self.params["corridor_chance"]: From e21d4d230c85aef5facca16e942057d2d0e51659 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 07:04:24 +0100 Subject: [PATCH 51/92] Add missing termination condition --- squirrelbattle/mapgeneration/broguelike.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index dff9a7c..e8b6fd1 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -165,6 +165,8 @@ class Generator: y, x = pos // width, pos % width if self.room_fits(level, y, x, room, door_y, door_x, dy, dx): self.place_room(level, y, x, room, door_y, door_x) + rooms_built += 1 + tries += 1 # post-processing self.place_walls(level) From 5ba07afc9ffa2a6e948ff0673c6ee4c8d03867ee Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 07:05:02 +0100 Subject: [PATCH 52/92] Fix typo in parameter names --- squirrelbattle/mapgeneration/broguelike.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index e8b6fd1..143911c 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -66,10 +66,10 @@ class Generator: def corr_meta_info(self): if random() < self.params["corridor_chance"]: - h_sup = randint(self.params["min_h_corr"], \ - self.params["max_h_corr"]) if random() < .5 else 0 - w_sup = 0 if h_sup else randint(self.params["min_w_corr"], \ - self.params["max_w_coor"]) + h_sup = randint(self.params["min_v_corr"], \ + self.params["max_v_corr"]) if random() < .5 else 0 + w_sup = 0 if h_sup else randint(self.params["min_h_corr"], \ + self.params["max_h_corr"]) h_off = h_sup if random() < .5 else 0 w_off = w_sup if random() < .5 else 0 return h_sup, w_sup, h_off, w_off From 605696dddd0888b960f6d81ef3ba54cf9cc91c23 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 07:36:31 +0100 Subject: [PATCH 53/92] Revamp door placing algorithm so that it generates cleaner doors; also remove lone starting room door from level --- squirrelbattle/mapgeneration/broguelike.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 143911c..b986b8f 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -89,18 +89,24 @@ class Generator: else: dx = -1 if random() < .5 else 1 - yxs = [i for i in range(len(room) * len(room[0]))] + rh, rw = len(room), len(room[0]) + yxs = [i for i in range(rh * rw)] shuffle(yxs) for pos in yxs: - y, x = pos // len(room[0]), pos % len(room[0]) + y, x = pos // rw, pos % rw if room[y][x] == Tile.EMPTY: - if room[y-dy][x-dx] == Tile.FLOOR: - build_here = True + # verify we are pointing away from a floor tile + if not(0 <= y-dy < rh and 0 <= x-dx < rw) or room[y-dy][x-dx] != Tile.FLOOR: + continue + # verify there's no other floor tile around us + for ny, nx in [[y+dy, x+dx], [y-dx, x-dy], [y+dx, x+dy]]: + if 0 <= ny < rh and 0 <= nx < rw and room[ny][nx] != Tile.EMPTY: + break + else: for i in range(l): if room[y+i*dy][x+i*dx] != Tile.EMPTY: - build_here = False break - if build_here: + else: for i in range(l): room[y+i*dy][x+i*dx] == Tile.FLOOR break @@ -147,6 +153,7 @@ class Generator: mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 starting_room, _, _, _, _ = self.create_random_room() self.place_room(level, height//2, width//2, starting_room, 0, 0) + level[0][0] = Tile.EMPTY self.params["corridor_chance"] = mem # find a starting position From 641f5c7872134f585883f6b76eaa1be16ff9c4f3 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 07:38:47 +0100 Subject: [PATCH 54/92] Make generation more sparse by asking for extra space around rooms; also add out of bounds option to Map.neighbourhood --- squirrelbattle/interfaces.py | 6 +++--- squirrelbattle/mapgeneration/broguelike.py | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 4e5f9ff..ad0e3b3 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -194,7 +194,7 @@ class Map: self.add_entity(dictclasses[entisave["type"]](**entisave)) @staticmethod - def neighbourhood(grid, y, x, large=False): + def neighbourhood(grid, y, x, large=False, oob=False): """ Returns up to 8 nearby coordinates, in a 3x3 square around the input coordinate if large is set to True, or in a 5-square cross by default. Does not return coordinates if they are out @@ -203,12 +203,12 @@ class Map: height, width = len(grid), len(grid[0]) neighbours = [] if large: - dyxs = product([-1, 0, 1], [-1, 0, 1]) + dyxs = [[dy, dx] for dy, dx in product([-1, 0, 1], [-1, 0, 1])] dyxs = dyxs[:5] + dyxs[6:] else: dyxs = [[0, -1], [0, 1], [-1, 0], [1, 0]] for dy, dx in dyxs: - if 0 <= y+dy < height and 0 <= x+dx < width: + if oob or (0 <= y+dy < height and 0 <= x+dx < width): neighbours.append([y+dy, x+dx]) return neighbours diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index b986b8f..96c7153 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -39,9 +39,16 @@ class Generator: for rx in range(rw): if room[ry][rx] == Tile.FLOOR: ly, lx = y + ry - door_y, x + rx - door_x + # tile must be in bounds and empty if not(0 <= ly < lh and 0 <= lx < lw) or \ level[ly][lx] == Tile.FLOOR: return False + # so do all neighbouring tiles bc we may + # need to place walls there eventually + for ny, nx in Map.neighbourhood(level, ly, lx, large=True, oob=True): + if not(0 <= ny < lh and 0 <= nx < lw) or \ + level[ny][nx] != Tile.EMPTY: + return False return True @staticmethod From c6947fab44b87f2f45b299d29bf7294b0bc704fb Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 07:39:52 +0100 Subject: [PATCH 55/92] Integrate the new map generation into the game ! Closes #5 --- squirrelbattle/game.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 4e097b1..e4544b0 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -12,7 +12,7 @@ import sys from .entities.player import Player from .enums import GameMode, KeyValues, DisplayActions from .interfaces import Map, Logs -from .mapgeneration import randomwalk +from .mapgeneration import randomwalk, broguelike from .resources import ResourceManager from .settings import Settings from . import menus @@ -51,7 +51,7 @@ class Game: """ Create a new game on the screen. """ - self.map = randomwalk.Generator().run() + self.map = broguelike.Generator().run() self.map.logs = self.logs self.logs.clear() self.player = Player() From c06f903a16d8a824b8e0d66f1d8f61eeaa43fef2 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 07:41:00 +0100 Subject: [PATCH 56/92] Fix a typo that made corridors unable to be built --- squirrelbattle/mapgeneration/broguelike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 96c7153..44a5716 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -115,7 +115,7 @@ class Generator: break else: for i in range(l): - room[y+i*dy][x+i*dx] == Tile.FLOOR + room[y+i*dy][x+i*dx] = Tile.FLOOR break return y+l*dy, x+l*dx, dy, dx From fab1bee8d82a5ac63bcb50bacabd68fe989f4edc Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 14:52:59 +0100 Subject: [PATCH 57/92] Force loop entrance to get coverage --- squirrelbattle/mapgeneration/randomwalk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 913599f..688c92c 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -96,8 +96,8 @@ class Generator: next_walker_pop.append(walker.split()) walkers = next_walker_pop - start_x, start_y = randint(0, width - 1), randint(0, height - 1) - while grid[start_y][start_x] != Tile.FLOOR: + start_x, start_y = -1, -1 + while grid[start_y][start_x] != Tile.FLOOR or start_x == -1: start_x, start_y = randint(0, width - 1), randint(0, height - 1) result = Map(width, height, grid, start_y, start_x) From 8d7e26438101da2db5f2b4cc8f9e5e0a86f13f6c Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 15:06:38 +0100 Subject: [PATCH 58/92] Fix a bug where the generator could crash by trying to place the starting room out of bounds; starting room position is now random --- squirrelbattle/mapgeneration/broguelike.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 44a5716..02fc736 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -159,7 +159,9 @@ class Generator: # the starting room must have no corridor mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 starting_room, _, _, _, _ = self.create_random_room() - self.place_room(level, height//2, width//2, starting_room, 0, 0) + dim_v, dim_h = len(starting_room), len(starting_room[0]) + pos_y, pos_x = randint(0, height-dim_v-1), randint(0, width-dim_h-1) + self.place_room(level, pos_y, pos_x, starting_room, 0, 0) level[0][0] = Tile.EMPTY self.params["corridor_chance"] = mem From dab84738d91e5a6b20132d1ba00de6ba8ecc3d8a Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 15:18:13 +0100 Subject: [PATCH 59/92] Remove the starting room door only if it really shouldn't be here; also account for the new randomized placement in removing lone door tile --- squirrelbattle/mapgeneration/broguelike.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 02fc736..59cbaeb 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -162,7 +162,8 @@ class Generator: dim_v, dim_h = len(starting_room), len(starting_room[0]) pos_y, pos_x = randint(0, height-dim_v-1), randint(0, width-dim_h-1) self.place_room(level, pos_y, pos_x, starting_room, 0, 0) - level[0][0] = Tile.EMPTY + if starting_room[0][0] != Tile.FLOOR: + level[pos_y][pos_x] = Tile.EMPTY self.params["corridor_chance"] = mem # find a starting position From 5424c7cd983314aeaff7ad477141de7433b1cfda Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 15:20:32 +0100 Subject: [PATCH 60/92] Nicer default parameters --- squirrelbattle/mapgeneration/broguelike.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 59cbaeb..191e757 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -8,13 +8,13 @@ from ..interfaces import Map, Tile DEFAULT_PARAMS = { - "width" : 80, - "height" : 40, - "tries" : 600, - "max_rooms" : 99, + "width" : 120, + "height" : 35, + "tries" : 300, + "max_rooms" : 20, "max_room_tries" : 15, "cross_room" : 1, - "corridor_chance" : .8, + "corridor_chance" : .6, "min_v_corr" : 2, "max_v_corr" : 6, "min_h_corr" : 4, From f240cafa833aebe3ab3ed43730beb5335e65f096 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 15:55:26 +0100 Subject: [PATCH 61/92] Fixing syntax in tests --- squirrelbattle/tests/mapgeneration_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 4fc6b28..394977b 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -28,5 +28,6 @@ class TestRandomWalk(unittest.TestCase): y, x = queue.pop() if m.tiles[y][x].can_walk(): m.tiles[y][x] = Tile.WALL - queue += m.neighbourhood(y, x) + queue += Map.neighbourhood(m.tiles, y, x) + self.assertFalse(any([any([t.can_walk() for t in l]) for l in m.tiles])) From 785ac403e354424b00143d6b6bc25aded8fac891 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 15:56:30 +0100 Subject: [PATCH 62/92] Forbid walker from ever reaching the outer most edge of the map --- squirrelbattle/mapgeneration/randomwalk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py index 688c92c..419d342 100644 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ b/squirrelbattle/mapgeneration/randomwalk.py @@ -48,7 +48,7 @@ class Walker: def move_in_bounds(self, width: int, height: int) -> None: nx, ny = self.next_pos() - if 0 < nx < width and 0 < ny < height: + if 0 < nx < width-1 and 0 < ny < height-1: self.x, self.y = nx, ny def split(self) -> "Walker": @@ -106,7 +106,7 @@ class Generator: for x in range(width): for y in range(height): if grid[y][x] == Tile.EMPTY: - c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in Map.neighbourhood(grid, y, x, large=True)]) + c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in Map.neighbourhood(grid, y, x)]) if c == 4 and self.params["no_lone_walls"]: result.tiles[y][x] = Tile.FLOOR elif c > 0: From 0aa4eb9c0b88369b3619bad368ebd27f042c6741 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 16:11:17 +0100 Subject: [PATCH 63/92] Refactoring in tests to allow for easy connexity verification --- squirrelbattle/tests/mapgeneration_test.py | 35 ++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 394977b..f546669 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -4,7 +4,22 @@ import unittest from squirrelbattle.interfaces import Map, Tile -from squirrelbattle.mapgeneration import randomwalk +from squirrelbattle.mapgeneration import randomwalk, broguelike + +def is_connex(grid): + h, w = len(grid), len(grid[0]) + y, x = randint(0, h-1), randint(0, w-1) + while not(grid[y][x].can_walk()): + y, x = randint(0, h-1), randint(0, w-1) + queue = Map.neighbourhood(grid, y, x) + while queue != []: + y, x = queue.pop() + if m.tiles[y][x].can_walk(): + m.tiles[y][x] = Tile.WALL + queue += Map.neighbourhood(grid, y, x) + return not(any([any([t.can_walk() for t in l]) for l in m.tiles])) + + class TestRandomWalk(unittest.TestCase): @@ -22,12 +37,14 @@ class TestRandomWalk(unittest.TestCase): m = self.generator.run() self.assertTrue(m.tiles[m.start_y][m.start_x].can_walk()) - #DFS - queue = Map.neighbourhood(m.tiles, m.start_y, m.start_x) - while queue != []: - y, x = queue.pop() - if m.tiles[y][x].can_walk(): - m.tiles[y][x] = Tile.WALL - queue += Map.neighbourhood(m.tiles, y, x) + def test_connexity(self) -> None: + m = self.generator.run() + self.assertTrue(is_connex(m.tiles)) - self.assertFalse(any([any([t.can_walk() for t in l]) for l in m.tiles])) +class TestBroguelike(unittest.TestCase): + def setUp(self) -> None: + self.generator = broguelike.Generator() + + def test_connexity(self) -> None: + m = self.generator.run() + self.assertTrue(is_connex(m)) From a390f4f5e9149273409594d3bb220ec9fa28764b Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 16:21:16 +0100 Subject: [PATCH 64/92] Fix is_connex tests --- squirrelbattle/tests/mapgeneration_test.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index f546669..85d1722 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import unittest +from random import randint from squirrelbattle.interfaces import Map, Tile from squirrelbattle.mapgeneration import randomwalk, broguelike @@ -14,13 +15,10 @@ def is_connex(grid): queue = Map.neighbourhood(grid, y, x) while queue != []: y, x = queue.pop() - if m.tiles[y][x].can_walk(): - m.tiles[y][x] = Tile.WALL + if grid[y][x].can_walk(): + grid[y][x] = Tile.WALL queue += Map.neighbourhood(grid, y, x) - return not(any([any([t.can_walk() for t in l]) for l in m.tiles])) - - - + return not(any([any([t.can_walk() for t in l]) for l in grid])) class TestRandomWalk(unittest.TestCase): def setUp(self) -> None: @@ -47,4 +45,4 @@ class TestBroguelike(unittest.TestCase): def test_connexity(self) -> None: m = self.generator.run() - self.assertTrue(is_connex(m)) + self.assertTrue(is_connex(m.tiles)) From c216a6089e60fcfcd0b429f2fe8e4a8e066c1d4e Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 16:51:04 +0100 Subject: [PATCH 65/92] Add a break so that generated rooms arre only placed once --- squirrelbattle/mapgeneration/broguelike.py | 1 + 1 file changed, 1 insertion(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 191e757..9d4cfd0 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -183,6 +183,7 @@ class Generator: if self.room_fits(level, y, x, room, door_y, door_x, dy, dx): self.place_room(level, y, x, room, door_y, door_x) rooms_built += 1 + break tries += 1 # post-processing From 9b853324adf91a6cce444a609110a9be53f9e9b0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 16:16:42 +0100 Subject: [PATCH 66/92] Drop first version of random walk --- squirrelbattle/game.py | 2 +- squirrelbattle/mapgeneration/randomwalk.py | 123 --------------------- squirrelbattle/tests/mapgeneration_test.py | 23 +--- 3 files changed, 3 insertions(+), 145 deletions(-) delete mode 100644 squirrelbattle/mapgeneration/randomwalk.py diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index e4544b0..bb917ec 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -12,7 +12,7 @@ import sys from .entities.player import Player from .enums import GameMode, KeyValues, DisplayActions from .interfaces import Map, Logs -from .mapgeneration import randomwalk, broguelike +from .mapgeneration import broguelike from .resources import ResourceManager from .settings import Settings from . import menus diff --git a/squirrelbattle/mapgeneration/randomwalk.py b/squirrelbattle/mapgeneration/randomwalk.py deleted file mode 100644 index 419d342..0000000 --- a/squirrelbattle/mapgeneration/randomwalk.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse -# SPDX-License-Identifier: GPL-3.0-or-later - -from enum import auto, Enum -from random import choice, random, randint -from typing import Tuple - -from ..interfaces import Map, Tile - - -DEFAULT_PARAMS = { - "split_chance": .15, - "turn_chance": .5, - "death_chance": .1, - "max_walkers": 15, - "width": 100, - "height": 100, - "fill": .4, - "no_lone_walls": False, -} - - -class Directions(Enum): - up = auto() - down = auto() - left = auto() - right = auto() - - -class Walker: - def __init__(self, x: int, y: int): - self.x = x - self.y = y - self.dir = choice(list(Directions)) - - def random_turn(self) -> None: - self.dir = choice(list(Directions)) - - def next_pos(self) -> Tuple[int, int]: - if self.dir == Directions.up: - return self.x, self.y + 1 - elif self.dir == Directions.down: - return self.x, self.y - 1 - elif self.dir == Directions.right: - return self.x + 1, self.y - elif self.dir == Directions.left: - return self.x - 1, self.y - - def move_in_bounds(self, width: int, height: int) -> None: - nx, ny = self.next_pos() - if 0 < nx < width-1 and 0 < ny < height-1: - self.x, self.y = nx, ny - - def split(self) -> "Walker": - child = Walker(self.x, self.y) - child.dir = self.dir - return child - - -class Generator: - def __init__(self, params: dict = DEFAULT_PARAMS): - self.params = params - - def run(self) -> Map: - width, height = self.params["width"], self.params["height"] - walkers = [Walker(width // 2, height // 2)] - grid = [[Tile.EMPTY for _ in range(width)] for _ in range(height)] - count = 0 - while count < self.params["fill"] * width * height: - # because we can't add or remove walkers while looping over the pop - # we need lists to keep track of what will be the walkers for the - # next iteration of the main loop - next_walker_pop = [] - - for walker in walkers: - if grid[walker.y][walker.x] == Tile.EMPTY: - count += 1 - grid[walker.y][walker.x] = Tile.FLOOR - if random() < self.params["turn_chance"]: - walker.random_turn() - walker.move_in_bounds(width, height) - if random() > self.params["death_chance"]: - next_walker_pop.append(walker) - - # we make sure to never kill all walkers - if not next_walker_pop: - next_walker_pop.append(choice(walkers)) - - # we use a second loop for spliting so we're not bothered by cases - # like a walker not spliting because we hit the population cap even - # though the next one would have died and freed a place - # not a big if it happened though - for walker in walkers: - if len(next_walker_pop) < self.params["max_walkers"]: - if random() < self.params["split_chance"]: - next_walker_pop.append(walker.split()) - walkers = next_walker_pop - - start_x, start_y = -1, -1 - while grid[start_y][start_x] != Tile.FLOOR or start_x == -1: - start_x, start_y = randint(0, width - 1), randint(0, height - 1) - - result = Map(width, height, grid, start_y, start_x) - - # post-processing: add walls - for x in range(width): - for y in range(height): - if grid[y][x] == Tile.EMPTY: - c = sum([1 if grid[j][i] == Tile.FLOOR else 0 for j, i in Map.neighbourhood(grid, y, x)]) - if c == 4 and self.params["no_lone_walls"]: - result.tiles[y][x] = Tile.FLOOR - elif c > 0: - result.tiles[y][x] = Tile.WALL - for x in range(width): - for y in [0, height-1]: - if grid[y][x] == Tile.FLOOR: - grid[y][x] = Tile.WALL - for y in range(height): - for x in [0, width-1]: - if grid[y][x] == Tile.FLOOR: - grid[y][x] = Tile.WALL - - return result diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 85d1722..062b2e0 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -5,7 +5,7 @@ import unittest from random import randint from squirrelbattle.interfaces import Map, Tile -from squirrelbattle.mapgeneration import randomwalk, broguelike +from squirrelbattle.mapgeneration import broguelike def is_connex(grid): h, w = len(grid), len(grid[0]) @@ -20,29 +20,10 @@ def is_connex(grid): queue += Map.neighbourhood(grid, y, x) return not(any([any([t.can_walk() for t in l]) for l in grid])) -class TestRandomWalk(unittest.TestCase): - def setUp(self) -> None: - #we set no_lone_walls to true for 100% coverage - params = randomwalk.DEFAULT_PARAMS - params["no_lone_walls"] = True - self.generator = randomwalk.Generator(params = params) - - def test_starting(self) -> None: - """ - Create a map and check that the whole map is accessible from the starting position using a - depth-first search - """ - m = self.generator.run() - self.assertTrue(m.tiles[m.start_y][m.start_x].can_walk()) - - def test_connexity(self) -> None: - m = self.generator.run() - self.assertTrue(is_connex(m.tiles)) - class TestBroguelike(unittest.TestCase): def setUp(self) -> None: self.generator = broguelike.Generator() - + def test_connexity(self) -> None: m = self.generator.run() self.assertTrue(is_connex(m.tiles)) From afaa9d17cdc8e0a8e50959d7951d55d8e2ec9a18 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 16:55:02 +0100 Subject: [PATCH 67/92] Linting --- squirrelbattle/interfaces.py | 14 +- squirrelbattle/mapgeneration/broguelike.py | 141 ++++++++++++--------- squirrelbattle/tests/mapgeneration_test.py | 28 ++-- 3 files changed, 101 insertions(+), 82 deletions(-) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index ad0e3b3..0802fb4 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -194,11 +194,13 @@ class Map: self.add_entity(dictclasses[entisave["type"]](**entisave)) @staticmethod - def neighbourhood(grid, y, x, large=False, oob=False): + def neighbourhood(grid: List[List["Tile"]], y: int, x: int, + large: bool = False, oob: bool = False) \ + -> List[List[int]]: """ - Returns up to 8 nearby coordinates, in a 3x3 square around the input coordinate if large is - set to True, or in a 5-square cross by default. Does not return coordinates if they are out - of bounds. + Returns up to 8 nearby coordinates, in a 3x3 square around the input + coordinate if large is set to True, or in a 5-square cross by default. + Does not return coordinates if they are out of bounds. """ height, width = len(grid), len(grid[0]) neighbours = [] @@ -208,8 +210,8 @@ class Map: else: dyxs = [[0, -1], [0, 1], [-1, 0], [1, 0]] for dy, dx in dyxs: - if oob or (0 <= y+dy < height and 0 <= x+dx < width): - neighbours.append([y+dy, x+dx]) + if oob or (0 <= y + dy < height and 0 <= x + dx < width): + neighbours.append([y + dy, x + dx]) return neighbours diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 9d4cfd0..b261f57 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -1,39 +1,42 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later -from enum import auto, Enum -from random import choice, random, randint, shuffle +from random import random, randint, shuffle +from typing import List, Tuple from ..interfaces import Map, Tile DEFAULT_PARAMS = { - "width" : 120, - "height" : 35, - "tries" : 300, - "max_rooms" : 20, - "max_room_tries" : 15, - "cross_room" : 1, - "corridor_chance" : .6, - "min_v_corr" : 2, - "max_v_corr" : 6, - "min_h_corr" : 4, - "max_h_corr" : 12, - "large_circular_room" : .10, - "circular_holes" : .5, + "width": 120, + "height": 35, + "tries": 300, + "max_rooms": 20, + "max_room_tries": 15, + "cross_room": 1, + "corridor_chance": .6, + "min_v_corr": 2, + "max_v_corr": 6, + "min_h_corr": 4, + "max_h_corr": 12, + "large_circular_room": .10, + "circular_holes": .5, } + class Generator: - def __init__(self, params: dict = DEFAULT_PARAMS): - self.params = params + def __init__(self, params: dict = None): + self.params = params or DEFAULT_PARAMS @staticmethod - def room_fits(level, y, x, room, door_y, door_x, dy, dx): + def room_fits(level: List[List[Tile]], y: int, x: int, + room: List[List[Tile]], door_y: int, door_x: int, + dy: int, dx: int) -> bool: lh, lw = len(level), len(level[0]) rh, rw = len(room), len(room[0]) - if not(0 < y+dy < lh and 0 < x+dx < lw): + if not(0 < y + dy < lh and 0 < x + dx < lw): return False - if level[y][x] != Tile.EMPTY or level[y+dy][x+dx] != Tile.FLOOR: + if level[y][x] != Tile.EMPTY or level[y + dy][x + dx] != Tile.FLOOR: return False for ry in range(rh): for rx in range(rw): @@ -45,24 +48,26 @@ class Generator: return False # so do all neighbouring tiles bc we may # need to place walls there eventually - for ny, nx in Map.neighbourhood(level, ly, lx, large=True, oob=True): + for ny, nx in Map.neighbourhood(level, ly, lx, + large=True, oob=True): if not(0 <= ny < lh and 0 <= nx < lw) or \ level[ny][nx] != Tile.EMPTY: return False return True @staticmethod - def place_room(level, y, x, room, door_y, door_x): + def place_room(level: List[List[Tile]], y: int, x: int, + room: List[List[Tile]], door_y: int, door_x: int) -> None: rh, rw = len(room), len(room[0]) # maybe place Tile.DOOR here ? level[y][x] = Tile.FLOOR for ry in range(rh): for rx in range(rw): if room[ry][rx] == Tile.FLOOR: - level[y-door_y+ry][x-door_x+rx] = Tile.FLOOR + level[y - door_y + ry][x - door_x + rx] = Tile.FLOOR @staticmethod - def place_walls(level): + def place_walls(level: List[List[Tile]]) -> None: h, w = len(level), len(level[0]) for y in range(h): for x in range(w): @@ -70,22 +75,24 @@ class Generator: for ny, nx in Map.neighbourhood(level, y, x): if level[ny][nx] == Tile.EMPTY: level[ny][nx] = Tile.WALL - - def corr_meta_info(self): + + def corr_meta_info(self) -> Tuple[int, int, int, int]: if random() < self.params["corridor_chance"]: - h_sup = randint(self.params["min_v_corr"], \ - self.params["max_v_corr"]) if random() < .5 else 0 - w_sup = 0 if h_sup else randint(self.params["min_h_corr"], \ - self.params["max_h_corr"]) + h_sup = randint(self.params["min_v_corr"], + self.params["max_v_corr"]) if random() < .5 else 0 + w_sup = 0 if h_sup else randint(self.params["min_h_corr"], + self.params["max_h_corr"]) h_off = h_sup if random() < .5 else 0 w_off = w_sup if random() < .5 else 0 return h_sup, w_sup, h_off, w_off return 0, 0, 0, 0 - def attach_door(self, room, h_sup, w_sup, h_off, w_off): - l = h_sup + w_sup + def attach_door(self, room: List[List[Tile]], h_sup: int, w_sup: int, + h_off: int, w_off: int) \ + -> Tuple[int, int, int, int]: + length = h_sup + w_sup dy, dx = 0, 0 - if l > 0: + if length > 0: if h_sup: dy = -1 if h_off else 1 else: @@ -103,77 +110,85 @@ class Generator: y, x = pos // rw, pos % rw if room[y][x] == Tile.EMPTY: # verify we are pointing away from a floor tile - if not(0 <= y-dy < rh and 0 <= x-dx < rw) or room[y-dy][x-dx] != Tile.FLOOR: + if not(0 <= y - dy < rh and 0 <= x - dx < rw) \ + or room[y - dy][x - dx] != Tile.FLOOR: continue # verify there's no other floor tile around us - for ny, nx in [[y+dy, x+dx], [y-dx, x-dy], [y+dx, x+dy]]: - if 0 <= ny < rh and 0 <= nx < rw and room[ny][nx] != Tile.EMPTY: + for ny, nx in [[y + dy, x + dx], [y - dx, x - dy], + [y + dx, x + dy]]: + if 0 <= ny < rh and 0 <= nx < rw \ + and room[ny][nx] != Tile.EMPTY: break else: - for i in range(l): - if room[y+i*dy][x+i*dx] != Tile.EMPTY: + for i in range(length): + if room[y + i * dy][x + i * dx] != Tile.EMPTY: break else: - for i in range(l): - room[y+i*dy][x+i*dx] = Tile.FLOOR + for i in range(length): + room[y + i * dy][x + i * dx] = Tile.FLOOR break - return y+l*dy, x+l*dx, dy, dx + return y + length * dy, x + length * dx, dy, dx - - def create_circular_room(self): + def create_circular_room(self) -> Tuple[List[List[Tile]], int, int, + int, int]: if random() < self.params["large_circular_room"]: r = randint(5, 10) else: r = randint(2, 4) room = [] - + h_sup, w_sup, h_off, w_off = self.corr_meta_info() - height = 2*r+2 - width = 2*r+2 + height = 2 * r + 2 + width = 2 * r + 2 make_hole = r > 6 and random() < self.params["circular_holes"] + r2 = 0 if make_hole: - r2 = randint(3, r-3) - for i in range(height+h_sup): + r2 = randint(3, r - 3) + for i in range(height + h_sup): room.append([]) - d = (i-h_off-height//2)**2 - for j in range(width+w_sup): - if d + (j-w_off-width//2)**2 < r**2 and \ - (not(make_hole) or d + (j-w_off-width//2)**2 >= r2**2): + d = (i - h_off - height // 2) ** 2 + for j in range(width + w_sup): + if d + (j - w_off - width // 2) ** 2 < r ** 2 and \ + (not make_hole + or d + (j - w_off - width // 2) ** 2 >= r2 ** 2): room[-1].append(Tile.FLOOR) else: room[-1].append(Tile.EMPTY) - door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup, h_off, w_off) + door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup, + h_off, w_off) return room, door_y, door_x, dy, dx - def create_random_room(self): + def create_random_room(self) -> Tuple[List[list], int, int, int, int]: return self.create_circular_room() - - def run(self): + + def run(self) -> Map: height, width = self.params["height"], self.params["width"] - level = [[Tile.EMPTY for i in range(width)] for j in range(height)] + level = [width * [Tile.EMPTY] for _ignored in range(height)] # the starting room must have no corridor mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 starting_room, _, _, _, _ = self.create_random_room() dim_v, dim_h = len(starting_room), len(starting_room[0]) - pos_y, pos_x = randint(0, height-dim_v-1), randint(0, width-dim_h-1) + pos_y, pos_x = randint(0, height - dim_v - 1),\ + randint(0, width - dim_h - 1) self.place_room(level, pos_y, pos_x, starting_room, 0, 0) if starting_room[0][0] != Tile.FLOOR: level[pos_y][pos_x] = Tile.EMPTY self.params["corridor_chance"] = mem - + # find a starting position - sy, sx = randint(0, height-1), randint(0, width-1) + sy, sx = randint(0, height - 1), randint(0, width - 1) while level[sy][sx] != Tile.FLOOR: - sy, sx = randint(0, height-1), randint(0, width-1) + sy, sx = randint(0, height - 1), randint(0, width - 1) # now we loop until we've tried enough, or we've added enough rooms tries, rooms_built = 0, 0 - while tries < self.params["tries"] and rooms_built < self.params["max_rooms"]: + while tries < self.params["tries"] \ + and rooms_built < self.params["max_rooms"]: room, door_y, door_x, dy, dx = self.create_random_room() positions = [i for i in range(height * width)] @@ -185,7 +200,7 @@ class Generator: rooms_built += 1 break tries += 1 - + # post-processing self.place_walls(level) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 062b2e0..fc1de8d 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -3,27 +3,29 @@ import unittest from random import randint +from typing import List from squirrelbattle.interfaces import Map, Tile from squirrelbattle.mapgeneration import broguelike -def is_connex(grid): - h, w = len(grid), len(grid[0]) - y, x = randint(0, h-1), randint(0, w-1) - while not(grid[y][x].can_walk()): - y, x = randint(0, h-1), randint(0, w-1) - queue = Map.neighbourhood(grid, y, x) - while queue != []: - y, x = queue.pop() - if grid[y][x].can_walk(): - grid[y][x] = Tile.WALL - queue += Map.neighbourhood(grid, y, x) - return not(any([any([t.can_walk() for t in l]) for l in grid])) class TestBroguelike(unittest.TestCase): def setUp(self) -> None: self.generator = broguelike.Generator() + def is_connex(self, grid: List[List[Tile]]) -> bool: + h, w = len(grid), len(grid[0]) + y, x = randint(0, h - 1), randint(0, w - 1) + while not (grid[y][x].can_walk()): + y, x = randint(0, h - 1), randint(0, w - 1) + queue = Map.neighbourhood(grid, y, x) + while queue: + y, x = queue.pop() + if grid[y][x].can_walk(): + grid[y][x] = Tile.WALL + queue += Map.neighbourhood(grid, y, x) + return not any([t.can_walk() for row in grid for t in row]) + def test_connexity(self) -> None: m = self.generator.run() - self.assertTrue(is_connex(m.tiles)) + self.assertTrue(self.is_connex(m.tiles)) From 8e7029e34d1ff9b96856943227e6ba5c2d9ea741 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 17:10:30 +0100 Subject: [PATCH 68/92] Fix walls --- squirrelbattle/mapgeneration/broguelike.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index b261f57..ca830b8 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -71,8 +71,8 @@ class Generator: h, w = len(level), len(level[0]) for y in range(h): for x in range(w): - if level[y][x] == Tile.FLOOR: - for ny, nx in Map.neighbourhood(level, y, x): + if not level[y][x].is_wall(): + for ny, nx in Map.neighbourhood(level, y, x, large=True): if level[ny][nx] == Tile.EMPTY: level[ny][nx] = Tile.WALL From df2c1a4b55232605fac936b4938fae3b2d724830 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 17:10:42 +0100 Subject: [PATCH 69/92] Add ladder on the start position --- squirrelbattle/mapgeneration/broguelike.py | 1 + 1 file changed, 1 insertion(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index ca830b8..38d462f 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -184,6 +184,7 @@ class Generator: sy, sx = randint(0, height - 1), randint(0, width - 1) while level[sy][sx] != Tile.FLOOR: sy, sx = randint(0, height - 1), randint(0, width - 1) + level[sy][sx] = Tile.LADDER # now we loop until we've tried enough, or we've added enough rooms tries, rooms_built = 0, 0 From 7e14122b8c38793ea6036ceb1bf9028ed0be1a00 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 17:25:52 +0100 Subject: [PATCH 70/92] Randomly place exit ladder --- squirrelbattle/mapgeneration/broguelike.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 38d462f..65eb337 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -205,4 +205,11 @@ class Generator: # post-processing self.place_walls(level) + # place an exit ladder + y, x = randint(0, height - 1), randint(0, width - 1) + while level[y][x] != Tile.FLOOR or \ + sum([t.can_walk() for t in Map.neighbourhood(level, y, x, large=True)]) < 5: + y, x = randint(0, height - 1), randint(0, width - 1) + level[sy][sx] = Tile.LADDER + return Map(width, height, level, sy, sx) From 9e099d071509378b0e276b31c5cffb6aa0c94586 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Fri, 8 Jan 2021 19:50:27 +0100 Subject: [PATCH 71/92] Ladders should spawn with no wall nearby --- squirrelbattle/mapgeneration/broguelike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 65eb337..2425df7 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -208,7 +208,7 @@ class Generator: # place an exit ladder y, x = randint(0, height - 1), randint(0, width - 1) while level[y][x] != Tile.FLOOR or \ - sum([t.can_walk() for t in Map.neighbourhood(level, y, x, large=True)]) < 5: + any([t.is_wall() for t in Map.neighbourhood(level, y, x, large=True)]): y, x = randint(0, height - 1), randint(0, width - 1) level[sy][sx] = Tile.LADDER From d8d0bc61908bf09d66028e147df3570445e0eab7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 19:20:56 +0100 Subject: [PATCH 72/92] Fix the end ladder --- squirrelbattle/mapgeneration/broguelike.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 2425df7..e3f86a7 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -208,8 +208,9 @@ class Generator: # place an exit ladder y, x = randint(0, height - 1), randint(0, width - 1) while level[y][x] != Tile.FLOOR or \ - any([t.is_wall() for t in Map.neighbourhood(level, y, x, large=True)]): + any([level[j][i].is_wall() for j, i + in Map.neighbourhood(level, y, x, large=True)]): y, x = randint(0, height - 1), randint(0, width - 1) - level[sy][sx] = Tile.LADDER + level[y][x] = Tile.LADDER return Map(width, height, level, sy, sx) From 571857b06301c58a7afdf31fbdeaf5363dda4558 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 20:00:42 +0100 Subject: [PATCH 73/92] Generate a random map when changing floor --- squirrelbattle/game.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index d51a7fc..7e5874c 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -53,7 +53,6 @@ class Game: """ Creates a new game on the screen. """ - # TODO generate a new map procedurally self.maps = [] self.map_index = 0 self.map = broguelike.Generator().run() @@ -188,9 +187,7 @@ class Game: self.map_index = 0 return while self.map_index >= len(self.maps): - # TODO: generate a new map - self.maps.append(Map.load(ResourceManager.get_asset_path( - "example_map_2.txt"))) + self.maps.append(broguelike.Generator().run()) new_map = self.map new_map.floor = self.map_index old_map.remove_entity(self.player) From 949555ffffb60ec70bf8b5889d12ec36c5ff8ef7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 20:06:32 +0100 Subject: [PATCH 74/92] Map at floor -1 is now not deterministic --- squirrelbattle/tests/game_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 2b8bbe4..ef81e44 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -745,8 +745,6 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.LADDER) self.assertEqual(self.game.map_index, 1) self.assertEqual(self.game.player.map.floor, 1) - self.assertEqual(self.game.player.y, 1) - self.assertEqual(self.game.player.x, 17) self.game.display_actions(DisplayActions.UPDATE) # Move up From ad3cce116e470d20548fab41cbe4e4f59944ebaf Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 21:23:12 +0100 Subject: [PATCH 75/92] Load map floor index when loading a new game --- squirrelbattle/game.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 7e5874c..fb10404 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -343,6 +343,8 @@ class Game: try: self.map_index = d["map_index"] self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]] + for i, m in enumerate(self.maps): + m.floor = i except KeyError: self.message = _("Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted.") From 26e66a5796c66a4b61de62330b1d04a9ec28fc6d Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Sun, 10 Jan 2021 21:30:18 +0100 Subject: [PATCH 76/92] Implement method add_loops along with tests --- squirrelbattle/mapgeneration/broguelike.py | 109 ++++++++++++++++----- squirrelbattle/tests/mapgeneration_test.py | 22 +++++ 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index e3f86a7..9d5bfaa 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -6,7 +6,6 @@ from typing import List, Tuple from ..interfaces import Map, Tile - DEFAULT_PARAMS = { "width": 120, "height": 35, @@ -21,8 +20,28 @@ DEFAULT_PARAMS = { "max_h_corr": 12, "large_circular_room": .10, "circular_holes": .5, + "loop_tries" : 40, + "loop_max" : 5, + "loop_threshold" : 15, } +def dist(level, y1, x1, y2, x2): + copy = [[t for t in l] for l in level] + dist = -1 + queue, next_queue = [[y1, x1]], [0] + while next_queue: + next_queue = [] + dist += 1 + while queue: + y, x = queue.pop() + copy[y][x] = Tile.EMPTY + if y == y2 and x == x2: + return dist + for y, x in Map.neighbourhood(copy, y, x): + if copy[y][x].can_walk(): + next_queue.append([y, x]) + queue = next_queue + return -1 class Generator: def __init__(self, params: dict = None): @@ -66,6 +85,39 @@ class Generator: if room[ry][rx] == Tile.FLOOR: level[y - door_y + ry][x - door_x + rx] = Tile.FLOOR + @staticmethod + def add_loop(level: List[List[Tile]], y: int, x: int) -> None: + h, w = len(level), len(level[0]) + if level[y][x] != Tile.EMPTY: + return False + # loop over both axis + for dx, dy in [[0, 1], [1, 0]]: + # then we find two floor tiles, exiting if we ever move oob + y1, x1, y2, x2 = y, x, y, x + while x1 >= 0 and y1 >= 0 and level[y1][x1] == Tile.EMPTY: + y1, x1 = y1 - dy, x1 - dx + while x2 < w and y2 < h and level[y2][x2] == Tile.EMPTY: + y2, x2 = y2 + dy, x2 + dx + if not(0 <= x1 <= x2 < w and 0 <= y1 <= y2 < h): + continue + + # if adding the path would make the two tiles significantly closer + # and its sides don't touch already placed terrain, build it + def verify_sides(): + for Dx, Dy in [[dy, dx], [-dy, -dx]]: + for i in range(1, y2-y1+x2-x1): + if not(0<= y1+Dy+i*dy < h and 0 <= x1+Dx+i*dx < w) or \ + level[y1+Dy+i*dy][x1+Dx+i*dx].can_walk(): + return False + return True + if dist(level, y1, x1, y2, x2) < 20 and verify_sides(): + y, x = y1+dy, x1+dx + while level[y][x] == Tile.EMPTY: + level[y][x] = Tile.FLOOR + y, x = y+dy, x+dx + return True + return False + @staticmethod def place_walls(level: List[List[Tile]]) -> None: h, w = len(level), len(level[0]) @@ -87,9 +139,29 @@ class Generator: return h_sup, w_sup, h_off, w_off return 0, 0, 0, 0 - def attach_door(self, room: List[List[Tile]], h_sup: int, w_sup: int, - h_off: int, w_off: int) \ - -> Tuple[int, int, int, int]: + @staticmethod + def build_door(room, y, x, dy, dx, length): + rh, rw = len(room), len(room[0]) + # verify we are pointing away from a floor tile + if not(0 <= y - dy < rh and 0 <= x - dx < rw) \ + or room[y - dy][x - dx] != Tile.FLOOR: + return False + # verify there's no other floor tile around us + for ny, nx in [[y + dy, x + dx], [y - dx, x - dy], + [y + dx, x + dy]]: + if 0 <= ny < rh and 0 <= nx < rw \ + and room[ny][nx] != Tile.EMPTY: + return False + for i in range(length): + if room[y + i * dy][x + i * dx] != Tile.EMPTY: + return False + for i in range(length): + room[y + i * dy][x + i * dx] = Tile.FLOOR + return True + + @staticmethod + def attach_door(room: List[List[Tile]], h_sup: int, w_sup: int, + h_off: int, w_off: int) -> Tuple[int, int, int, int]: length = h_sup + w_sup dy, dx = 0, 0 if length > 0: @@ -108,25 +180,10 @@ class Generator: shuffle(yxs) for pos in yxs: y, x = pos // rw, pos % rw - if room[y][x] == Tile.EMPTY: - # verify we are pointing away from a floor tile - if not(0 <= y - dy < rh and 0 <= x - dx < rw) \ - or room[y - dy][x - dx] != Tile.FLOOR: - continue - # verify there's no other floor tile around us - for ny, nx in [[y + dy, x + dx], [y - dx, x - dy], - [y + dx, x + dy]]: - if 0 <= ny < rh and 0 <= nx < rw \ - and room[ny][nx] != Tile.EMPTY: - break - else: - for i in range(length): - if room[y + i * dy][x + i * dx] != Tile.EMPTY: - break - else: - for i in range(length): - room[y + i * dy][x + i * dx] = Tile.FLOOR - break + if room[y][x] == Tile.EMPTY and \ + Generator.build_door(room, y, x, dy, dx, length): + break + return y + length * dy, x + length * dx, dy, dx def create_circular_room(self) -> Tuple[List[List[Tile]], int, int, @@ -204,6 +261,12 @@ class Generator: # post-processing self.place_walls(level) + tries, loops = 0, 0 + while tries < self.params["loop_tries"] and \ + loops < self.params["loop_max"]: + tries += 1 + y, x = randint(0, height-1), randint(0, width-1) + loops += self.add_loop(level, y, x) # place an exit ladder y, x = randint(0, height - 1), randint(0, width - 1) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index fc1de8d..b58e10c 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -7,11 +7,19 @@ from typing import List from squirrelbattle.interfaces import Map, Tile from squirrelbattle.mapgeneration import broguelike +from squirrelbattle.display.texturepack import TexturePack class TestBroguelike(unittest.TestCase): def setUp(self) -> None: self.generator = broguelike.Generator() + self.stom = lambda x : Map.load_from_string("0 0\n" + x) + self.mtos = lambda x : x.draw_string(TexturePack.ASCII_PACK) + + def test_dist(self): + m = self.stom(".. ..\n ... ") + distance = broguelike.dist(m.tiles, 0, 0, 0, 4) + self.assertEqual(distance, 6) def is_connex(self, grid: List[List[Tile]]) -> bool: h, w = len(grid), len(grid[0]) @@ -29,3 +37,17 @@ class TestBroguelike(unittest.TestCase): def test_connexity(self) -> None: m = self.generator.run() self.assertTrue(self.is_connex(m.tiles)) + + def test_doors(self) -> None: + # corridors shouldn't loop back into the room + pass + + def test_loops(self) -> None: + m = self.stom(3*".. ..\n") + self.generator.add_loop(m.tiles, 1, 3) + s = self.mtos(m) + self.assertEqual(s, ".. ..\n.......\n.. ..") + self.assertFalse(self.generator.add_loop(m.tiles, 0, 0)) + m = self.stom("...\n. .\n...") + self.assertFalse(self.generator.add_loop(m.tiles, 1, 1)) + From 96bbc5088f62ee2c9113fb8463d0d82bc3c7edca Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Sun, 10 Jan 2021 21:32:58 +0100 Subject: [PATCH 77/92] Add a test case for non connex maps in distance computation --- squirrelbattle/tests/mapgeneration_test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index b58e10c..07ed05d 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -20,6 +20,9 @@ class TestBroguelike(unittest.TestCase): m = self.stom(".. ..\n ... ") distance = broguelike.dist(m.tiles, 0, 0, 0, 4) self.assertEqual(distance, 6) + m = self.stom(". .") + distance = broguelike.dist(m.tiles, 0, 0, 0, 2) + self.assertEqual(distance, -1) def is_connex(self, grid: List[List[Tile]]) -> bool: h, w = len(grid), len(grid[0]) @@ -38,10 +41,6 @@ class TestBroguelike(unittest.TestCase): m = self.generator.run() self.assertTrue(self.is_connex(m.tiles)) - def test_doors(self) -> None: - # corridors shouldn't loop back into the room - pass - def test_loops(self) -> None: m = self.stom(3*".. ..\n") self.generator.add_loop(m.tiles, 1, 3) From e639ad62553a37f4b93f90bf5afa37f44057340b Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Sun, 10 Jan 2021 21:48:12 +0100 Subject: [PATCH 78/92] Getting to full cover, and minor fix to bug that allowed corridors to create loops in a room, resulting in implacability --- squirrelbattle/mapgeneration/broguelike.py | 2 +- squirrelbattle/tests/mapgeneration_test.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 9d5bfaa..d5be37f 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -152,7 +152,7 @@ class Generator: if 0 <= ny < rh and 0 <= nx < rw \ and room[ny][nx] != Tile.EMPTY: return False - for i in range(length): + for i in range(length+1): if room[y + i * dy][x + i * dx] != Tile.EMPTY: return False for i in range(length): diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 07ed05d..5ee4eb3 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -37,6 +37,10 @@ class TestBroguelike(unittest.TestCase): queue += Map.neighbourhood(grid, y, x) return not any([t.can_walk() for row in grid for t in row]) + def test_build_doors(self): + m = self.stom(". .\n. .\n. .\n") + self.assertFalse(self.generator.build_door(m.tiles, 1, 1, 0, 1, 2)) + def test_connexity(self) -> None: m = self.generator.run() self.assertTrue(self.is_connex(m.tiles)) From 12e19759aa808892236f3509e9b8df7783b98d91 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Sun, 10 Jan 2021 21:49:39 +0100 Subject: [PATCH 79/92] Implement populate method, so map generation also handles entity spawn --- squirrelbattle/mapgeneration/broguelike.py | 50 +++++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index d5be37f..7fe9f88 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -1,10 +1,10 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later -from random import random, randint, shuffle +from random import random, randint, shuffle, choice, choices from typing import List, Tuple -from ..interfaces import Map, Tile +from ..interfaces import Map, Tile, Entity DEFAULT_PARAMS = { "width": 120, @@ -23,6 +23,7 @@ DEFAULT_PARAMS = { "loop_tries" : 40, "loop_max" : 5, "loop_threshold" : 15, + "spawn_per_region" : [1, 2], } def dist(level, y1, x1, y2, x2): @@ -46,6 +47,8 @@ def dist(level, y1, x1, y2, x2): class Generator: def __init__(self, params: dict = None): self.params = params or DEFAULT_PARAMS + self.spawn_areas = [] + self.queued_area = None @staticmethod def room_fits(level: List[List[Tile]], y: int, x: int, @@ -186,8 +189,8 @@ class Generator: return y + length * dy, x + length * dx, dy, dx - def create_circular_room(self) -> Tuple[List[List[Tile]], int, int, - int, int]: + def create_circular_room(self, spawnable: bool = True) \ + -> Tuple[List[List[Tile]], int, int, int, int]: if random() < self.params["large_circular_room"]: r = randint(5, 10) else: @@ -214,21 +217,49 @@ class Generator: else: room[-1].append(Tile.EMPTY) + if spawnable: + self.register_spawn_area(room) door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup, h_off, w_off) return room, door_y, door_x, dy, dx - def create_random_room(self) -> Tuple[List[list], int, int, int, int]: + def create_random_room(self, spawnable: bool = True) \ + -> Tuple[List[list], int, int, int, int]: return self.create_circular_room() + def register_spawn_area(self, area:List[List[Tile]]): + spawn_positions = [] + for y, line in enumerate(area): + for x, tile in enumerate(line): + if tile == Tile.FLOOR: + spawn_positions.append([y, x]) + self.queued_area = spawn_positions + + def update_spawnable(self, y, x): + if self.queued_area != None: + translated_area = [[y+ry, x+rx] for ry, rx in self.queued_area] + self.spawn_areas.append(translated_area) + self.queued_area = None + + def populate(self, rv): + min_c, max_c = self.params["spawn_per_region"] + for region in self.spawn_areas: + entity_count = randint(min_c, max_c) + for _dummy in range(entity_count): + entity = choices(Entity.get_all_entity_classes(), + weights=Entity.get_weights(), k=1)[0]() + y, x = choice(region) + entity.move(y, x) + rv.add_entity(entity) + def run(self) -> Map: height, width = self.params["height"], self.params["width"] level = [width * [Tile.EMPTY] for _ignored in range(height)] # the starting room must have no corridor mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 - starting_room, _, _, _, _ = self.create_random_room() + starting_room, _, _, _, _ = self.create_random_room(spawnable = False) dim_v, dim_h = len(starting_room), len(starting_room[0]) pos_y, pos_x = randint(0, height - dim_v - 1),\ randint(0, width - dim_h - 1) @@ -254,6 +285,7 @@ class Generator: for pos in positions: y, x = pos // width, pos % width if self.room_fits(level, y, x, room, door_y, door_x, dy, dx): + self.update_spawnable(y - door_y, x - door_x) self.place_room(level, y, x, room, door_y, door_x) rooms_built += 1 break @@ -276,4 +308,8 @@ class Generator: y, x = randint(0, height - 1), randint(0, width - 1) level[y][x] = Tile.LADDER - return Map(width, height, level, sy, sx) + # spawn entities + rv = Map(width, height, level, sy, sx) + self.populate(rv) + + return rv From 01cdea6edcbf6e48a2ef61a1d1523dd999bea5b2 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 21:57:51 +0100 Subject: [PATCH 80/92] Don't spawn random entities on each level anymore --- squirrelbattle/entities/player.py | 3 --- squirrelbattle/game.py | 1 - squirrelbattle/interfaces.py | 16 ---------------- 3 files changed, 20 deletions(-) diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 615dfd5..81a0c55 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -71,9 +71,6 @@ class Player(InventoryHolder, FightingEntity): self.max_xp = self.level * 10 self.health = self.maxhealth self.strength = self.strength + 1 - # TODO Remove it, that's only fun - self.map.spawn_random_entities(randint(3 * self.level, - 10 * self.level)) def add_xp(self, xp: int) -> None: """ diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index fb10404..87317e0 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -61,7 +61,6 @@ class Game: self.player = Player() self.map.add_entity(self.player) self.player.move(self.map.start_y, self.map.start_x) - self.map.spawn_random_entities(randint(3, 10)) self.inventory_menu.update_player(self.player) @property diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index e9b2407..6e3065c 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -180,22 +180,6 @@ class Map: return "\n".join("".join(tile.char(pack) for tile in line) for line in self.tiles) - def spawn_random_entities(self, count: int) -> None: - """ - Puts randomly {count} entities on the map, only on empty ground tiles. - """ - for _ignored in range(count): - y, x = 0, 0 - while True: - y, x = randint(0, self.height - 1), randint(0, self.width - 1) - tile = self.tiles[y][x] - if tile.can_walk(): - break - entity = choices(Entity.get_all_entity_classes(), - weights=Entity.get_weights(), k=1)[0]() - entity.move(y, x) - self.add_entity(entity) - def compute_visibility(self, y: int, x: int, max_range: int) -> None: """ Sets the visible tiles to be the ones visible by an entity at point From 9df1ac78832498838617b9b440ab738a1e044236 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 22:08:42 +0100 Subject: [PATCH 81/92] Linting --- squirrelbattle/game.py | 1 - squirrelbattle/interfaces.py | 2 +- squirrelbattle/mapgeneration/broguelike.py | 58 ++++++++++++---------- squirrelbattle/tests/mapgeneration_test.py | 11 ++-- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 87317e0..89818d0 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later from json import JSONDecodeError -from random import randint from typing import Any, Optional, List import curses import json diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 6e3065c..ba9be08 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -4,7 +4,7 @@ from enum import Enum, auto from math import ceil, sqrt from itertools import product -from random import choice, choices, randint +from random import choice, randint from typing import List, Optional, Any, Dict, Tuple from queue import PriorityQueue from functools import reduce diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 7fe9f88..d192d3c 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -20,14 +20,15 @@ DEFAULT_PARAMS = { "max_h_corr": 12, "large_circular_room": .10, "circular_holes": .5, - "loop_tries" : 40, - "loop_max" : 5, - "loop_threshold" : 15, - "spawn_per_region" : [1, 2], + "loop_tries": 40, + "loop_max": 5, + "loop_threshold": 15, + "spawn_per_region": [1, 2], } -def dist(level, y1, x1, y2, x2): - copy = [[t for t in l] for l in level] + +def dist(level: List[List[Tile]], y1: int, x1: int, y2: int, x2: int) -> int: + copy = [[t for t in row] for row in level] dist = -1 queue, next_queue = [[y1, x1]], [0] while next_queue: @@ -36,7 +37,7 @@ def dist(level, y1, x1, y2, x2): while queue: y, x = queue.pop() copy[y][x] = Tile.EMPTY - if y == y2 and x == x2: + if y == y2 and x == x2: return dist for y, x in Map.neighbourhood(copy, y, x): if copy[y][x].can_walk(): @@ -44,6 +45,7 @@ def dist(level, y1, x1, y2, x2): queue = next_queue return -1 + class Generator: def __init__(self, params: dict = None): self.params = params or DEFAULT_PARAMS @@ -89,7 +91,7 @@ class Generator: level[y - door_y + ry][x - door_x + rx] = Tile.FLOOR @staticmethod - def add_loop(level: List[List[Tile]], y: int, x: int) -> None: + def add_loop(level: List[List[Tile]], y: int, x: int) -> bool: h, w = len(level), len(level[0]) if level[y][x] != Tile.EMPTY: return False @@ -106,18 +108,21 @@ class Generator: # if adding the path would make the two tiles significantly closer # and its sides don't touch already placed terrain, build it - def verify_sides(): - for Dx, Dy in [[dy, dx], [-dy, -dx]]: - for i in range(1, y2-y1+x2-x1): - if not(0<= y1+Dy+i*dy < h and 0 <= x1+Dx+i*dx < w) or \ - level[y1+Dy+i*dy][x1+Dx+i*dx].can_walk(): + def verify_sides() -> bool: + for delta_x, delta_y in [[dy, dx], [-dy, -dx]]: + for i in range(1, y2 - y1 + x2 - x1): + if not (0 <= y1 + delta_y + i * dy < h + and 0 <= x1 + delta_x + i * dx < w) or \ + level[y1 + delta_y + i * dy][x1 + delta_x + + i * dx]\ + .can_walk(): return False return True if dist(level, y1, x1, y2, x2) < 20 and verify_sides(): - y, x = y1+dy, x1+dx + y, x = y1 + dy, x1 + dx while level[y][x] == Tile.EMPTY: level[y][x] = Tile.FLOOR - y, x = y+dy, x+dx + y, x = y + dy, x + dx return True return False @@ -143,7 +148,8 @@ class Generator: return 0, 0, 0, 0 @staticmethod - def build_door(room, y, x, dy, dx, length): + def build_door(room: List[List[Tile]], y: int, x: int, + dy: int, dx: int, length: int) -> bool: rh, rw = len(room), len(room[0]) # verify we are pointing away from a floor tile if not(0 <= y - dy < rh and 0 <= x - dx < rw) \ @@ -155,7 +161,7 @@ class Generator: if 0 <= ny < rh and 0 <= nx < rw \ and room[ny][nx] != Tile.EMPTY: return False - for i in range(length+1): + for i in range(length + 1): if room[y + i * dy][x + i * dx] != Tile.EMPTY: return False for i in range(length): @@ -163,8 +169,8 @@ class Generator: return True @staticmethod - def attach_door(room: List[List[Tile]], h_sup: int, w_sup: int, - h_off: int, w_off: int) -> Tuple[int, int, int, int]: + def attach_door(room: List[List[Tile]], h_sup: int, w_sup: int, + h_off: int, w_off: int) -> Tuple[int, int, int, int]: length = h_sup + w_sup dy, dx = 0, 0 if length > 0: @@ -228,7 +234,7 @@ class Generator: -> Tuple[List[list], int, int, int, int]: return self.create_circular_room() - def register_spawn_area(self, area:List[List[Tile]]): + def register_spawn_area(self, area: List[List[Tile]]) -> None: spawn_positions = [] for y, line in enumerate(area): for x, tile in enumerate(line): @@ -236,13 +242,13 @@ class Generator: spawn_positions.append([y, x]) self.queued_area = spawn_positions - def update_spawnable(self, y, x): - if self.queued_area != None: - translated_area = [[y+ry, x+rx] for ry, rx in self.queued_area] + def update_spawnable(self, y: int, x: int) -> None: + if self.queued_area is not None: + translated_area = [[y + ry, x + rx] for ry, rx in self.queued_area] self.spawn_areas.append(translated_area) self.queued_area = None - def populate(self, rv): + def populate(self, rv: Map) -> None: min_c, max_c = self.params["spawn_per_region"] for region in self.spawn_areas: entity_count = randint(min_c, max_c) @@ -259,7 +265,7 @@ class Generator: # the starting room must have no corridor mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 - starting_room, _, _, _, _ = self.create_random_room(spawnable = False) + starting_room, _, _, _, _ = self.create_random_room(spawnable=False) dim_v, dim_h = len(starting_room), len(starting_room[0]) pos_y, pos_x = randint(0, height - dim_v - 1),\ randint(0, width - dim_h - 1) @@ -297,7 +303,7 @@ class Generator: while tries < self.params["loop_tries"] and \ loops < self.params["loop_max"]: tries += 1 - y, x = randint(0, height-1), randint(0, width-1) + y, x = randint(0, height - 1), randint(0, width - 1) loops += self.add_loop(level, y, x) # place an exit ladder diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 5ee4eb3..141864d 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -13,10 +13,10 @@ from squirrelbattle.display.texturepack import TexturePack class TestBroguelike(unittest.TestCase): def setUp(self) -> None: self.generator = broguelike.Generator() - self.stom = lambda x : Map.load_from_string("0 0\n" + x) - self.mtos = lambda x : x.draw_string(TexturePack.ASCII_PACK) + self.stom = lambda x: Map.load_from_string("0 0\n" + x) + self.mtos = lambda x: x.draw_string(TexturePack.ASCII_PACK) - def test_dist(self): + def test_dist(self) -> None: m = self.stom(".. ..\n ... ") distance = broguelike.dist(m.tiles, 0, 0, 0, 4) self.assertEqual(distance, 6) @@ -37,7 +37,7 @@ class TestBroguelike(unittest.TestCase): queue += Map.neighbourhood(grid, y, x) return not any([t.can_walk() for row in grid for t in row]) - def test_build_doors(self): + def test_build_doors(self) -> None: m = self.stom(". .\n. .\n. .\n") self.assertFalse(self.generator.build_door(m.tiles, 1, 1, 0, 1, 2)) @@ -46,11 +46,10 @@ class TestBroguelike(unittest.TestCase): self.assertTrue(self.is_connex(m.tiles)) def test_loops(self) -> None: - m = self.stom(3*".. ..\n") + m = self.stom(3 * ".. ..\n") self.generator.add_loop(m.tiles, 1, 3) s = self.mtos(m) self.assertEqual(s, ".. ..\n.......\n.. ..") self.assertFalse(self.generator.add_loop(m.tiles, 0, 0)) m = self.stom("...\n. .\n...") self.assertFalse(self.generator.add_loop(m.tiles, 1, 1)) - From 0ea10546ace7b34f60e90086ae9100dafd4a00c3 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 22:19:15 +0100 Subject: [PATCH 82/92] Fix merge issues --- squirrelbattle/game.py | 2 +- squirrelbattle/interfaces.py | 2 +- squirrelbattle/mapgeneration/broguelike.py | 4 ++-- squirrelbattle/tests/mapgeneration_test.py | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index ce5ce53..6b8bc0f 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -420,7 +420,7 @@ class Game: except KeyError as error: self.message = _("Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted.")\ - + f"\n{error}" + + f"\n{error}" os.unlink(ResourceManager.get_config_path("save.json")) self.display_actions(DisplayActions.UPDATE) return diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index b6a472c..a17ab45 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -7,7 +7,7 @@ from functools import reduce from itertools import product from math import ceil, sqrt from queue import PriorityQueue -from random import choice, choices, randint +from random import choice, randint from typing import Any, Dict, List, Optional, Tuple from .display.texturepack import TexturePack diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index d192d3c..8ae6582 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -1,10 +1,10 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later -from random import random, randint, shuffle, choice, choices +from random import choice, choices, randint, random, shuffle from typing import List, Tuple -from ..interfaces import Map, Tile, Entity +from ..interfaces import Entity, Map, Tile DEFAULT_PARAMS = { "width": 120, diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 141864d..d840b69 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -1,13 +1,13 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later -import unittest from random import randint from typing import List +import unittest -from squirrelbattle.interfaces import Map, Tile -from squirrelbattle.mapgeneration import broguelike -from squirrelbattle.display.texturepack import TexturePack +from ..display.texturepack import TexturePack +from ..interfaces import Map, Tile +from ..mapgeneration import broguelike class TestBroguelike(unittest.TestCase): From 5e378fc2d02f87b6d5581cf4e4c24e4a6592fa76 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 22:27:46 +0100 Subject: [PATCH 83/92] Update game rules --- docs/rules.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/rules.rst b/docs/rules.rst index 77cfc6b..4b86976 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -11,8 +11,9 @@ prêt à tout pour s'en sortir. Sa vision de rongeur lui permet d'observer l'intégralité de la carte_, et à l'aide d'objets_, il va pouvoir affronter les monstres_ présents dans le donjon et gagner en expérience et en force. -Le jeu fonctionne par niveau. À chaque niveau ``n`` du joueur, entre ``3n`` et -``7n`` entités apparaissent aléatoirement sur la carte. +Le jeu fonctionne par étage. À chaque étage, différents monstres sont présents, +et à l'aide d'objets, il pourra progresser dans le donjon et descendre de plus +en plus bas. En tuant des ennemis, ce qu'il parvient à faire en fonçant directement sur eux ayant mangé trop de noisettes (ou étant armé d'un couteau), l'écureuil va From e74431086157a792b442f76f20587a30b387b7c3 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 22:51:01 +0100 Subject: [PATCH 84/92] Place doors at the beginning of the corridor --- squirrelbattle/display/texturepack.py | 3 +++ squirrelbattle/interfaces.py | 1 + squirrelbattle/mapgeneration/broguelike.py | 3 +-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 16cad4f..d8d04cc 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -82,6 +82,7 @@ TexturePack.ASCII_PACK = TexturePack( BOW=')', CHEST='□', CHESTPLATE='(', + DOOR='&', EAGLE='µ', EMPTY=' ', EXPLOSION='%', @@ -124,6 +125,8 @@ TexturePack.SQUIRREL_PACK = TexturePack( BOW='🏹', CHEST='🧰', CHESTPLATE='🦺', + DOOR=('🚪', curses.COLOR_WHITE, (1000, 1000, 1000), + curses.COLOR_WHITE, (1000, 1000, 1000)), EAGLE='🦅', EMPTY=' ', EXPLOSION='💥', diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index a17ab45..ea39cac 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -390,6 +390,7 @@ class Tile(Enum): WALL = auto() FLOOR = auto() LADDER = auto() + DOOR = auto() @staticmethod def from_ascii_char(ch: str) -> "Tile": diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 8ae6582..c537cf8 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -83,8 +83,7 @@ class Generator: def place_room(level: List[List[Tile]], y: int, x: int, room: List[List[Tile]], door_y: int, door_x: int) -> None: rh, rw = len(room), len(room[0]) - # maybe place Tile.DOOR here ? - level[y][x] = Tile.FLOOR + level[y][x] = Tile.DOOR for ry in range(rh): for rx in range(rw): if room[ry][rx] == Tile.FLOOR: From 6c0aaffd77b315e0cb285e45709c138dab915514 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 22:53:27 +0100 Subject: [PATCH 85/92] Doors are walls --- squirrelbattle/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index ea39cac..80be06b 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -431,7 +431,7 @@ class Tile(Enum): """ Is this Tile a wall? """ - return self == Tile.WALL + return self == Tile.WALL or self == Tile.DOOR def is_ladder(self) -> bool: """ From 099a0eab3177c3d3f1a82c58d8ad635640a8c963 Mon Sep 17 00:00:00 2001 From: Charles Peyrat Date: Sun, 10 Jan 2021 22:54:48 +0100 Subject: [PATCH 86/92] Add comments and docstring --- squirrelbattle/mapgeneration/broguelike.py | 116 +++++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 7fe9f88..cc7ddae 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -27,6 +27,10 @@ DEFAULT_PARAMS = { } def dist(level, y1, x1, y2, x2): + """ + Compute the minimum walking distance between points (y1, x1) and (y2, x2) on a Tile grid + """ + # simple breadth first search copy = [[t for t in l] for l in level] dist = -1 queue, next_queue = [[y1, x1]], [0] @@ -54,12 +58,19 @@ class Generator: def room_fits(level: List[List[Tile]], y: int, x: int, room: List[List[Tile]], door_y: int, door_x: int, dy: int, dx: int) -> bool: + """ + Using point (door_y, door_x) in the room as a reference and placing it + over point (y, x) in the level, returns whether or not the room fits + here + """ lh, lw = len(level), len(level[0]) rh, rw = len(room), len(room[0]) if not(0 < y + dy < lh and 0 < x + dx < lw): return False + # door must be placed on an empty tile, and point into a floor tile if level[y][x] != Tile.EMPTY or level[y + dy][x + dx] != Tile.FLOOR: return False + # now we verify floor tiles in both grids do not overlap for ry in range(rh): for rx in range(rw): if room[ry][rx] == Tile.FLOOR: @@ -80,8 +91,12 @@ class Generator: @staticmethod def place_room(level: List[List[Tile]], y: int, x: int, room: List[List[Tile]], door_y: int, door_x: int) -> None: + """ + Mutates level in place to add the room. Placement is determined by + making (door_y, door_x) in the room correspond with (y, x) in the level + """ rh, rw = len(room), len(room[0]) - # maybe place Tile.DOOR here ? + # maybe place Tile.DOOR here instead ? level[y][x] = Tile.FLOOR for ry in range(rh): for rx in range(rw): @@ -90,12 +105,20 @@ class Generator: @staticmethod def add_loop(level: List[List[Tile]], y: int, x: int) -> None: + """ + Try to add a corridor between two far apart floor tiles, passing + through point (y, x). + """ h, w = len(level), len(level[0]) + if level[y][x] != Tile.EMPTY: return False - # loop over both axis + + # loop over both directions, trying to place both veritcal + # and horizontal corridors for dx, dy in [[0, 1], [1, 0]]: - # then we find two floor tiles, exiting if we ever move oob + # then we find two floor tiles, one on each side of (y, x) + # exiting if we don't find two (reach the edge of the map before) y1, x1, y2, x2 = y, x, y, x while x1 >= 0 and y1 >= 0 and level[y1][x1] == Tile.EMPTY: y1, x1 = y1 - dy, x1 - dx @@ -104,15 +127,18 @@ class Generator: if not(0 <= x1 <= x2 < w and 0 <= y1 <= y2 < h): continue - # if adding the path would make the two tiles significantly closer - # and its sides don't touch already placed terrain, build it def verify_sides(): + # switching up dy and dx here pivots the axis, so + # (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to + # (y, x), but not on the original axis for Dx, Dy in [[dy, dx], [-dy, -dx]]: for i in range(1, y2-y1+x2-x1): if not(0<= y1+Dy+i*dy < h and 0 <= x1+Dx+i*dx < w) or \ level[y1+Dy+i*dy][x1+Dx+i*dx].can_walk(): return False return True + # if adding the path would make the two tiles significantly closer + # and its sides don't touch already placed terrain, build it if dist(level, y1, x1, y2, x2) < 20 and verify_sides(): y, x = y1+dy, x1+dx while level[y][x] == Tile.EMPTY: @@ -123,6 +149,10 @@ class Generator: @staticmethod def place_walls(level: List[List[Tile]]) -> None: + """ + Place wall tiles on every empty tile that is adjacent (in the largest + sense), to a floor tile + """ h, w = len(level), len(level[0]) for y in range(h): for x in range(w): @@ -132,11 +162,25 @@ class Generator: level[ny][nx] = Tile.WALL def corr_meta_info(self) -> Tuple[int, int, int, int]: + """ + Return info about the extra grid space that should be allocated for the + room, and where the room should be built along this extra grid space. + Because grids are usually thight around the room, this gives us extra + place to add a corridor later. Corridor length and orientation is + implicitly derived from this info. + + h_sup and w_sup represent the extra needed space along each axis, + and h_off and w_off are the offset at which to build the room + """ if random() < self.params["corridor_chance"]: h_sup = randint(self.params["min_v_corr"], self.params["max_v_corr"]) if random() < .5 else 0 + # we only allow extra space allocation along one axis, + # because there won't more than one exit corridor w_sup = 0 if h_sup else randint(self.params["min_h_corr"], self.params["max_h_corr"]) + # implicitly choose which direction along the axis + # the corridor will be pointing to h_off = h_sup if random() < .5 else 0 w_off = w_sup if random() < .5 else 0 return h_sup, w_sup, h_off, w_off @@ -144,6 +188,12 @@ class Generator: @staticmethod def build_door(room, y, x, dy, dx, length): + """ + Tries to build the exit from the room at given coordinates + Depending on parameter length, it will either attempt to build a + simple door, or a long corridor. Return value is a boolean + signifying whether or not the exit was successfully built + """ rh, rw = len(room), len(room[0]) # verify we are pointing away from a floor tile if not(0 <= y - dy < rh and 0 <= x - dx < rw) \ @@ -155,6 +205,7 @@ class Generator: if 0 <= ny < rh and 0 <= nx < rw \ and room[ny][nx] != Tile.EMPTY: return False + # see if the path ahead is clear. needed in the case of non convex room for i in range(length+1): if room[y + i * dy][x + i * dx] != Tile.EMPTY: return False @@ -165,6 +216,10 @@ class Generator: @staticmethod def attach_door(room: List[List[Tile]], h_sup: int, w_sup: int, h_off: int, w_off: int) -> Tuple[int, int, int, int]: + """ + Attach an exit to the room. If extra space was allocated to + the grid, make sure a corridor is properly built + """ length = h_sup + w_sup dy, dx = 0, 0 if length > 0: @@ -173,11 +228,13 @@ class Generator: else: dx = -1 if w_off else 1 else: + # determine door direction for rooms without corridors if random() < .5: dy = -1 if random() < .5 else 1 else: dx = -1 if random() < .5 else 1 + # loop over all possible positions in a random order rh, rw = len(room), len(room[0]) yxs = [i for i in range(rh * rw)] shuffle(yxs) @@ -186,11 +243,18 @@ class Generator: if room[y][x] == Tile.EMPTY and \ Generator.build_door(room, y, x, dy, dx, length): break + else: + return None, None return y + length * dy, x + length * dx, dy, dx def create_circular_room(self, spawnable: bool = True) \ -> Tuple[List[List[Tile]], int, int, int, int]: + """ + Create and return as a tile grid a room that is circular in shape, and + may have a center, also circular hole + Also return door info so we know how to place the room in the level + """ if random() < self.params["large_circular_room"]: r = randint(5, 10) else: @@ -217,8 +281,11 @@ class Generator: else: room[-1].append(Tile.EMPTY) + # log all placed tiles as spawn positions if spawnable: self.register_spawn_area(room) + + # attach exit door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup, h_off, w_off) @@ -226,9 +293,18 @@ class Generator: def create_random_room(self, spawnable: bool = True) \ -> Tuple[List[list], int, int, int, int]: + """ + Randomly select a room shape and return one such room along with its + door info. Set spawnable to False is the room should be marked as a + potential spawning region on the map + """ return self.create_circular_room() def register_spawn_area(self, area:List[List[Tile]]): + """ + Register all floor positions relative to the input grid + for later use + """ spawn_positions = [] for y, line in enumerate(area): for x, tile in enumerate(line): @@ -237,12 +313,22 @@ class Generator: self.queued_area = spawn_positions def update_spawnable(self, y, x): + """ + Convert previous spawn positions relative to the room grid to actual + actual spawn positions on the level grid, using the position of the + top left corner of the room on the level, then log them as a + spawnable region + """ if self.queued_area != None: translated_area = [[y+ry, x+rx] for ry, rx in self.queued_area] self.spawn_areas.append(translated_area) self.queued_area = None def populate(self, rv): + """ + Populate every spawnable area with some randomly chosen, randomly + placed entity + """ min_c, max_c = self.params["spawn_per_region"] for region in self.spawn_areas: entity_count = randint(min_c, max_c) @@ -254,6 +340,10 @@ class Generator: rv.add_entity(entity) def run(self) -> Map: + """ + Using procedural generation, build and return a full map, populated + with entities + """ height, width = self.params["height"], self.params["width"] level = [width * [Tile.EMPTY] for _ignored in range(height)] @@ -261,24 +351,30 @@ class Generator: mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 starting_room, _, _, _, _ = self.create_random_room(spawnable = False) dim_v, dim_h = len(starting_room), len(starting_room[0]) + # because Generator.room_fits checks that the exit door is correctly + # placed, but the starting room has no exit door, we find a positoin + # manually pos_y, pos_x = randint(0, height - dim_v - 1),\ randint(0, width - dim_h - 1) self.place_room(level, pos_y, pos_x, starting_room, 0, 0) + # remove the door that was placed if starting_room[0][0] != Tile.FLOOR: level[pos_y][pos_x] = Tile.EMPTY self.params["corridor_chance"] = mem - # find a starting position + # find a starting position for the player sy, sx = randint(0, height - 1), randint(0, width - 1) while level[sy][sx] != Tile.FLOOR: sy, sx = randint(0, height - 1), randint(0, width - 1) level[sy][sx] = Tile.LADDER - # now we loop until we've tried enough, or we've added enough rooms + # now we loop until we're bored, or we've added enough rooms tries, rooms_built = 0, 0 while tries < self.params["tries"] \ and rooms_built < self.params["max_rooms"]: + # build a room, try to fit it everywhere in a random order, and + # place it at the first possible position room, door_y, door_x, dy, dx = self.create_random_room() positions = [i for i in range(height * width)] shuffle(positions) @@ -293,6 +389,12 @@ class Generator: # post-processing self.place_walls(level) + + # because when a room is placed, it leads to exactly one previously + # placed room, the level has a tree like structure with the starting + # room as the root + # to avoid boring player backtracking, we add some cycles to the room + # graph in post processing by placing additional corridors tries, loops = 0, 0 while tries < self.params["loop_tries"] and \ loops < self.params["loop_max"]: From 11daa8573ceabe55f7aaf346be60246fc5862c36 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 22:59:34 +0100 Subject: [PATCH 87/92] The players can open doors --- squirrelbattle/display/texturepack.py | 2 +- squirrelbattle/entities/player.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index d8d04cc..92df405 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -126,7 +126,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( CHEST='🧰', CHESTPLATE='🦺', DOOR=('🚪', curses.COLOR_WHITE, (1000, 1000, 1000), - curses.COLOR_WHITE, (1000, 1000, 1000)), + curses.COLOR_WHITE, (1000, 1000, 1000)), EAGLE='🦅', EMPTY=' ', EXPLOSION='💥', diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 17ee7df..a04eed2 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -6,7 +6,7 @@ from random import randint from typing import Dict, Optional, Tuple from .items import Item -from ..interfaces import FightingEntity, InventoryHolder +from ..interfaces import FightingEntity, InventoryHolder, Tile from ..translations import gettext as _ @@ -152,6 +152,12 @@ class Player(InventoryHolder, FightingEntity): return True elif entity.is_item(): entity.hold(self) + tile = self.map.tiles[y][x] + if tile == Tile.DOOR and move_if_possible: + # Open door + self.map.tiles[y][x] = Tile.FLOOR + self.map.compute_visibility(y, x, self.vision) + return super().check_move(y, x, move_if_possible) return super().check_move(y, x, move_if_possible) def save_state(self) -> dict: From 8f845d1e4cb32c257ab496d15727c7a19d6ced88 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 23:03:24 +0100 Subject: [PATCH 88/92] Doors don't break the connexity of map --- squirrelbattle/tests/mapgeneration_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index d840b69..3879f8f 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -27,15 +27,16 @@ class TestBroguelike(unittest.TestCase): def is_connex(self, grid: List[List[Tile]]) -> bool: h, w = len(grid), len(grid[0]) y, x = randint(0, h - 1), randint(0, w - 1) - while not (grid[y][x].can_walk()): + while not (grid[y][x].can_walk() or grid[y][x] == Tile.DOOR): y, x = randint(0, h - 1), randint(0, w - 1) queue = Map.neighbourhood(grid, y, x) while queue: y, x = queue.pop() - if grid[y][x].can_walk(): + if grid[y][x].can_walk() or grid[y][x] == Tile.DOOR: grid[y][x] = Tile.WALL queue += Map.neighbourhood(grid, y, x) - return not any([t.can_walk() for row in grid for t in row]) + return not any([t.can_walk() or t == Tile.DOOR + for row in grid for t in row]) def test_build_doors(self) -> None: m = self.stom(". .\n. .\n. .\n") From b0ca1d4edfa7835107f0b4f345dce0b92778a2d0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 23:05:49 +0100 Subject: [PATCH 89/92] Cover everytime the map generation test --- squirrelbattle/tests/mapgeneration_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/tests/mapgeneration_test.py b/squirrelbattle/tests/mapgeneration_test.py index 3879f8f..5fa19fd 100644 --- a/squirrelbattle/tests/mapgeneration_test.py +++ b/squirrelbattle/tests/mapgeneration_test.py @@ -26,8 +26,8 @@ class TestBroguelike(unittest.TestCase): def is_connex(self, grid: List[List[Tile]]) -> bool: h, w = len(grid), len(grid[0]) - y, x = randint(0, h - 1), randint(0, w - 1) - while not (grid[y][x].can_walk() or grid[y][x] == Tile.DOOR): + y, x = -1, -1 + while not grid[y][x].can_walk(): y, x = randint(0, h - 1), randint(0, w - 1) queue = Map.neighbourhood(grid, y, x) while queue: From 60675d78593e69c0d89cbc4def3047cd7b9ac5d2 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 23:21:28 +0100 Subject: [PATCH 90/92] Cover doors code --- squirrelbattle/assets/example_map.txt | 6 +++--- squirrelbattle/tests/entities_test.py | 6 +++--- squirrelbattle/tests/game_test.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/squirrelbattle/assets/example_map.txt b/squirrelbattle/assets/example_map.txt index be2e798..68c3ae1 100644 --- a/squirrelbattle/assets/example_map.txt +++ b/squirrelbattle/assets/example_map.txt @@ -2,17 +2,17 @@ ####### ############# #.H...# #...........# #.....# #####...........# - #.....# #............H..# + #.....# #...&........H..# #.##### #.###...........# #.# #.# #...........# #.# #.# ############# #.# #.# #.#### #.# #....# #.# - ####.###################.# + ####&###################&# #.....................# ################# #.....................# #...............# #.....................#######...............# - #...........................................# + #.....................&.....&...............# #.....................#######...............# ####################### ################# diff --git a/squirrelbattle/tests/entities_test.py b/squirrelbattle/tests/entities_test.py index a0e2548..396d87c 100644 --- a/squirrelbattle/tests/entities_test.py +++ b/squirrelbattle/tests/entities_test.py @@ -134,13 +134,13 @@ class TestEntities(unittest.TestCase): self.map.remove_entity(entity2) # Test following the player and finding the player as target - self.player.move(5, 5) - fam.move(4, 5) + self.player.move(6, 5) + fam.move(5, 5) fam.target = None self.player.move_down() self.map.tick(self.player) self.assertTrue(fam.target == self.player) - self.assertEqual(fam.y, 5) + self.assertEqual(fam.y, 6) self.assertEqual(fam.x, 5) # Test random move diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index dbeaa9e..5716854 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -728,6 +728,7 @@ class TestGame(unittest.TestCase): self.game.player.inventory.clear() ring = RingCritical() ring.hold(self.game.player) + self.game.display_actions(DisplayActions.REFRESH) old_critical = self.game.player.critical self.game.handle_key_pressed(KeyValues.EQUIP) self.assertEqual(self.game.player.critical, @@ -951,3 +952,18 @@ class TestGame(unittest.TestCase): # Exit the menu self.game.handle_key_pressed(KeyValues.SPACE) self.assertEqual(self.game.state, GameMode.PLAY) + + def test_doors(self) -> None: + """ + Check that the user can open doors. + """ + self.game.state = GameMode.PLAY + + self.game.player.move(9, 8) + self.assertEqual(self.game.map.tiles[10][8], Tile.DOOR) + # Open door + self.game.handle_key_pressed(KeyValues.DOWN) + self.assertEqual(self.game.map.tiles[10][8], Tile.FLOOR) + self.assertEqual(self.game.player.y, 10) + self.assertEqual(self.game.player.x, 8) + self.game.display_actions(DisplayActions.REFRESH) From 65ae99a26db613fad98cf83a8c74d2865711470e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 23:41:51 +0100 Subject: [PATCH 91/92] The logs of the map was not updated --- squirrelbattle/game.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 6b8bc0f..2e467fb 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -199,7 +199,9 @@ class Game: self.map_index = 0 return while self.map_index >= len(self.maps): - self.maps.append(broguelike.Generator().run()) + m = broguelike.Generator().run() + m.logs = self.logs + self.maps.append(m) new_map = self.map new_map.floor = self.map_index old_map.remove_entity(self.player) @@ -417,6 +419,7 @@ class Game: self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]] for i, m in enumerate(self.maps): m.floor = i + m.logs = self.logs except KeyError as error: self.message = _("Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted.")\ From 588357e5bf9d6d0154701c9ce337230330e7a8e8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sun, 10 Jan 2021 23:49:43 +0100 Subject: [PATCH 92/92] Linting --- squirrelbattle/mapgeneration/broguelike.py | 45 ++++++++++------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/squirrelbattle/mapgeneration/broguelike.py b/squirrelbattle/mapgeneration/broguelike.py index 4a97e44..f746f37 100644 --- a/squirrelbattle/mapgeneration/broguelike.py +++ b/squirrelbattle/mapgeneration/broguelike.py @@ -26,9 +26,11 @@ DEFAULT_PARAMS = { "spawn_per_region": [1, 2], } -def dist(level, y1, x1, y2, x2): + +def dist(level: List[List[Tile]], y1: int, x1: int, y2: int, x2: int) -> int: """ - Compute the minimum walking distance between points (y1, x1) and (y2, x2) on a Tile grid + Compute the minimum walking distance between points (y1, x1) and (y2, x2) + on a Tile grid """ # simple breadth first search copy = [[t for t in row] for row in level] @@ -60,9 +62,9 @@ class Generator: room: List[List[Tile]], door_y: int, door_x: int, dy: int, dx: int) -> bool: """ - Using point (door_y, door_x) in the room as a reference and placing it + Using point (door_y, door_x) in the room as a reference and placing it over point (y, x) in the level, returns whether or not the room fits - here + here """ lh, lw = len(level), len(level[0]) rh, rw = len(room), len(room[0]) @@ -93,7 +95,7 @@ class Generator: def place_room(level: List[List[Tile]], y: int, x: int, room: List[List[Tile]], door_y: int, door_x: int) -> None: """ - Mutates level in place to add the room. Placement is determined by + Mutates level in place to add the room. Placement is determined by making (door_y, door_x) in the room correspond with (y, x) in the level """ rh, rw = len(room), len(room[0]) @@ -106,11 +108,11 @@ class Generator: @staticmethod def add_loop(level: List[List[Tile]], y: int, x: int) -> bool: """ - Try to add a corridor between two far apart floor tiles, passing + Try to add a corridor between two far apart floor tiles, passing through point (y, x). """ h, w = len(level), len(level[0]) - + if level[y][x] != Tile.EMPTY: return False @@ -128,8 +130,8 @@ class Generator: continue def verify_sides() -> bool: - # switching up dy and dx here pivots the axis, so - # (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to + # switching up dy and dx here pivots the axis, so + # (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to # (y, x), but not on the original axis for delta_x, delta_y in [[dy, dx], [-dy, -dx]]: for i in range(1, y2 - y1 + x2 - x1): @@ -194,8 +196,8 @@ class Generator: dy: int, dx: int, length: int) -> bool: """ Tries to build the exit from the room at given coordinates - Depending on parameter length, it will either attempt to build a - simple door, or a long corridor. Return value is a boolean + Depending on parameter length, it will either attempt to build a + simple door, or a long corridor. Return value is a boolean signifying whether or not the exit was successfully built """ rh, rw = len(room), len(room[0]) @@ -247,15 +249,15 @@ class Generator: if room[y][x] == Tile.EMPTY and \ Generator.build_door(room, y, x, dy, dx, length): break - else: - return None, None + else: # pragma: no cover + return None, None, None, None return y + length * dy, x + length * dx, dy, dx def create_circular_room(self, spawnable: bool = True) \ -> Tuple[List[List[Tile]], int, int, int, int]: """ - Create and return as a tile grid a room that is circular in shape, and + Create and return as a tile grid a room that is circular in shape, and may have a center, also circular hole Also return door info so we know how to place the room in the level """ @@ -298,7 +300,7 @@ class Generator: def create_random_room(self, spawnable: bool = True) \ -> Tuple[List[list], int, int, int, int]: """ - Randomly select a room shape and return one such room along with its + Randomly select a room shape and return one such room along with its door info. Set spawnable to False is the room should be marked as a potential spawning region on the map """ @@ -319,12 +321,12 @@ class Generator: def update_spawnable(self, y: int, x: int) -> None: """ Convert previous spawn positions relative to the room grid to actual - actual spawn positions on the level grid, using the position of the - top left corner of the room on the level, then log them as a + actual spawn positions on the level grid, using the position of the + top left corner of the room on the level, then log them as a spawnable region """ - if self.queued_area != None: - translated_area = [[y+ry, x+rx] for ry, rx in self.queued_area] + if self.queued_area is not None: + translated_area = [[y + ry, x + rx] for ry, rx in self.queued_area] self.spawn_areas.append(translated_area) self.queued_area = None @@ -333,11 +335,6 @@ class Generator: Populate every spawnable area with some randomly chosen, randomly placed entity """ - if self.queued_area is not None: - translated_area = [[y + ry, x + rx] for ry, rx in self.queued_area] - self.spawn_areas.append(translated_area) - self.queued_area = None - min_c, max_c = self.params["spawn_per_region"] for region in self.spawn_areas: entity_count = randint(min_c, max_c)