diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e92d4a3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Python application + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install pipenv + uses: dschep/install-pipenv-action@v1 + - name: Install dependencies + run: | + pipenv install --dev + #- name: Lint with flake8 + # run: | + # pip install flake8 + # # stop the build if there are Python syntax errors or undefined names + # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pipenv run pytest diff --git a/.gitignore b/.gitignore index b6e4761..9fe17bc 100644 --- a/.gitignore +++ b/.gitignore @@ -126,4 +126,4 @@ venv.bak/ dmypy.json # Pyre type checker -.pyre/ +.pyre/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a3e76b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "${workspaceFolder}/.venv/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..15ab76c --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" +pylint = "*" + +[packages] +opensimplex = "*" +colorama = "*" + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..946f9d1 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,162 @@ +{ + "_meta": { + "hash": { + "sha256": "596d8d7058d0416cf65ac46be46c706f8a2c885e732ee27d614f6b98631253f5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "index": "pypi", + "version": "==0.4.3" + }, + "opensimplex": { + "hashes": [ + "sha256:53f12be10faecd6158b406b7af3a66f992435c3b86fca6041f5a14c7979e172b" + ], + "index": "pypi", + "version": "==0.2" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", + "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" + ], + "version": "==2.3.3" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "version": "==4.3.21" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "version": "==1.4.3" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", + "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" + ], + "version": "==8.2.0" + }, + "packaging": { + "hashes": [ + "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", + "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" + ], + "version": "==20.1" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + ], + "version": "==1.8.1" + }, + "pylint": { + "hashes": [ + "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", + "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" + ], + "index": "pypi", + "version": "==2.4.4" + }, + "pyparsing": { + "hashes": [ + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, + "pytest": { + "hashes": [ + "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", + "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" + ], + "index": "pypi", + "version": "==5.3.5" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "wcwidth": { + "hashes": [ + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + ], + "version": "==0.1.8" + }, + "wrapt": { + "hashes": [ + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + ], + "version": "==1.11.2" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3eef7f3 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Running Locally + +``` +PIPENV_VENV_IN_PROJECT=1 +pipenv install --dev +... +``` + +# Testing + +``` +pipenv run pytest +``` + diff --git a/main_package/field.py b/main_package/field.py index 7fa9c4e..b40ae8c 100644 --- a/main_package/field.py +++ b/main_package/field.py @@ -11,25 +11,34 @@ logging.basicConfig(level=logging.INFO) class FieldTypeEnum(Enum): - EMPTY = Fore.LIGHTBLACK_EX + "E" BASE = Fore.RED + "B" ANT = Fore.BLUE + "A" FOOD = Fore.GREEN + "F" + GRASS = Fore.LIGHTBLACK_EX + "g" + FOREST = Fore.LIGHTBLACK_EX + "F" + WATER = Fore.LIGHTBLACK_EX + "w" + DEEP_WATER = Fore.LIGHTBLACK_EX + "W" + ROCK = Fore.LIGHTBLACK_EX + "R" + SAND = Fore.LIGHTBLACK_EX + "S" + DRY_GRASS = Fore.LIGHTBLACK_EX + "D" + TALL_GRASS = Fore.LIGHTBLACK_EX + "G" + class Field: log = logging.getLogger(__name__) - def __init__(self, xpos:int, ypos:int): + def __init__(self, xpos:int, ypos:int, type: FieldTypeEnum): self.xpos = xpos self.ypos = ypos - self.type = FieldTypeEnum.EMPTY + self.emptyType = type + self.type = type self.entity = None def getPos(self) -> Tuple[int, int]: return self.xpos, self.ypos def resetToEmpty(self): - self.type = FieldTypeEnum.EMPTY + self.type = self.emptyType self.entity = None def setEntity(self, entity) -> bool: diff --git a/main_package/gameBoard.py b/main_package/gameBoard.py index 4379581..f932694 100644 --- a/main_package/gameBoard.py +++ b/main_package/gameBoard.py @@ -1,6 +1,7 @@ from typing import Tuple, List, Set from main_package.fieldEntities.ant import Ant from main_package.field import * +from main_package.mapGenerator import * from main_package.fieldEntities.base import Base from main_package.fieldEntities.food import Food from main_package.interfaces.attackable import Attackable @@ -9,10 +10,11 @@ class gameBoard: + resolution = 5 log = logging.getLogger(__name__) validForAttack = [FieldTypeEnum.ANT, FieldTypeEnum.BASE] - def __init__(self, xdim: int = 10, ydim: int = 10): + def __init__(self, xdim: int = 50, ydim: int = 40): """ Initializes an empty board of the given dimensions :param xdim: x dimension (exclusive) @@ -20,7 +22,9 @@ def __init__(self, xdim: int = 10, ydim: int = 10): """ self.xdim = xdim self.ydim = ydim - self.gameBoard = [[Field(xpos=x, ypos=y) for x in range(xdim)] for y in range(ydim)] + + mapGen = MapGenerator(xdim, ydim) + self.gameBoard = mapGen.map self.ants: dict[str, Ant] = {} self.playerBases = {} self.players = [] @@ -49,8 +53,8 @@ def createBase(self, xpos: int, ypos: int, player: str) -> bool: # field where base is placed must be empty field = self.getField(xpos, ypos) - if field.type != FieldTypeEnum.EMPTY: - logging.error("Base cannot be placed on field that is not empty. Field is {}".format(field.type)) + if field.type != FieldTypeEnum.GRASS: + logging.error("Base cannot be placed on field that is not grass. Field is {}".format(field.type)) return False if player in self.playerBases.keys(): @@ -130,8 +134,8 @@ def createAnt(self, xpos: int, ypos: int, antId: str, player: str) -> bool: if not any(f.type == FieldTypeEnum.BASE for f in neighbouring_fields): self.log.error("Invalid Placement, no adjacent base") return False - elif placementDesitnation.type is not FieldTypeEnum.EMPTY: - self.log.error("Invalid Placement, field not empty") + elif placementDesitnation.type is not FieldTypeEnum.GRASS: + self.log.error("Invalid Placement, field not grass") return False # check if player owns base near which they want to place ant @@ -156,7 +160,7 @@ def moveAnt(self, antId: str, xpos: int, ypos: int) -> bool: ant = self.ants[antId] # determine valid fields for movement fields = self.getNeighbouringFields(ant.fieldPosition) - validFields = filter(lambda x: x.type == FieldTypeEnum.EMPTY, fields) + validFields = filter(lambda x: x.type != FieldTypeEnum.ROCK, fields) # is movement valid ? fieldToMoveTo: Field = None @@ -231,7 +235,7 @@ def getBase(self, playerName: str) -> Base or None: def createFood(self, xpos: int, ypos: int, magnitude: int) -> bool: targetField = self.getField(xpos, ypos) - if targetField is None or targetField.type is not FieldTypeEnum.EMPTY: + if targetField is None or targetField.type is not FieldTypeEnum.GRASS: self.log.error("Invalid target ({},{}) for placing food.".format(xpos, ypos)) return False if magnitude <= 0 or magnitude != magnitude: # test for negative or nan diff --git a/main_package/mapGenerator.py b/main_package/mapGenerator.py new file mode 100644 index 0000000..3b3c99e --- /dev/null +++ b/main_package/mapGenerator.py @@ -0,0 +1,82 @@ +from main_package.field import * +from opensimplex import OpenSimplex +from math import * + +class MapGenerator: + mapScale = 30 # bigger => softer land feature + moistureScale = 70 # bigger => softer land feature + + waterMaxElevation = 20 + sandMaxElevation = 2 + + rockMinElevation = 60 + + grassMinMoisture = 45 + tallGrassMinElevation = 55 + tallGrassMinMoisture = 40 + + forestMinMoisture = 0 + forestMaxMoisture = 0 + forestMaxElevation = 0 + + def __init__(self, width: int, height: int): + """ + Initializes a map board of the given dimensions + :param xdim: width (exclusive) + :param ydim: height (exclusive) + """ + self.width = width + self.height = height + self.noiseGenerator = OpenSimplex() + self.map = self.createMap() + + def getTerrain(self, elevation: float, moisture: float) -> FieldTypeEnum: + e = elevation * 100 # elevation [0, 100] + m = moisture * 100 # moisture [0, 100] + + if (e < self.waterMaxElevation / 3): + return FieldTypeEnum.DEEP_WATER + if (e < self.waterMaxElevation): + return FieldTypeEnum.WATER + if (e < self.waterMaxElevation + self.sandMaxElevation): + return FieldTypeEnum.SAND + + if (e > self.rockMinElevation): + return FieldTypeEnum.ROCK + if (e > self.rockMinElevation - self.sandMaxElevation): + return FieldTypeEnum.TALL_GRASS + + if (m < self.grassMinMoisture): + return FieldTypeEnum.DRY_GRASS + if (e < self.forestMaxElevation and m > self.forestMinMoisture and m < self.forestMaxMoisture): + return FieldTypeEnum.FOREST + if (e > self.tallGrassMinElevation and m > self.tallGrassMinMoisture): + return FieldTypeEnum.TALL_GRASS + + return FieldTypeEnum.GRASS + + def createMap(self): + map = [] + + for y in range(0, self.height): + col = [] + for x in range(0, self.width): + elevationValue = self.getNoise(x, y, self.mapScale) + moistureValue = self.getNoise(x, y, self.moistureScale) + + # Now use the noise values to determine the block type + terrainType = self.getTerrain(elevationValue, moistureValue) + + col.append( + Field(xpos=x, ypos=y, type=terrainType) + ) + + map.append(col) + + return map + + def getNoise(self, x: int, y: int, noiseScale: int): + return self.noiseGenerator.noise2d( + x = x / noiseScale, + y = y / noiseScale + ) / 2 + 0.5 \ No newline at end of file diff --git a/main_package/test_gameBoard.py b/main_package/test_gameBoard.py index 2a470d4..7548267 100644 --- a/main_package/test_gameBoard.py +++ b/main_package/test_gameBoard.py @@ -120,7 +120,7 @@ def test_ant_attack(self): self.assertTrue(board.attack("A", 0, 1)) self.assertTrue(antB.health == 0) board.tick() - self.assertTrue(antBField.type == FieldTypeEnum.EMPTY) + self.assertTrue(antBField.type == FieldTypeEnum.GRASS) self.assertTrue(board.getAnt("B") is None) # dead ants removed from board # test attacking and killing base @@ -133,7 +133,7 @@ def test_ant_attack(self): self.assertTrue(base.health == 5) self.assertTrue(board.attack("A", 1, 1)) board.tick() - self.assertTrue(baseField.type == FieldTypeEnum.EMPTY) + self.assertTrue(baseField.type == FieldTypeEnum.GRASS) self.assertTrue(board.getBase("testPlayer") is None) base.health = 0 @@ -170,7 +170,7 @@ def test_ant_feed(self): self.assertTrue(board.feed("C", 0, 3)) self.assertFalse(board.feed("C", 0, 3)) # food source empty self.assertTrue(antC.currentFood == 5) - self.assertTrue(board.getField(0, 3).type == FieldTypeEnum.EMPTY) # check food source depleation + self.assertTrue(board.getField(0, 3).type == FieldTypeEnum.GRASS) # check food source depleation def test_getAntIdsOfPlayer(self): board = gameBoard(5, 5)