├── .gitignore ├── README.md ├── demo ├── rpg1.png └── rpg2.png ├── main.py ├── resources └── graphics │ ├── armorman.png │ ├── bandit.png │ ├── devil.png │ ├── fireball.png │ ├── footman.png │ ├── magician.png │ ├── mouse.png │ └── tile.png └── source ├── AStarSearch.py ├── __init__.py ├── component ├── __init__.py ├── entity.py └── map.py ├── constants.py ├── data ├── entity.json └── map │ ├── level_1.json │ ├── level_2.json │ ├── level_3.json │ ├── level_4.json │ └── level_5.json ├── gameAI.py ├── main.py ├── state ├── __init__.py └── level.py └── tool.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PythonStrategyRPG 2 | It is a simple strategy turn based game 3 | * use AStar algorithm to walk 4 | * support melee and remote creature 5 | * use json file to store level data (e.g. position of creatures, map info) 6 | 7 | # Requirement 8 | * Python 3.7 9 | * Python-Pygame 1.9 10 | 11 | # How To Start Game 12 | $ python main.py 13 | 14 | # How to Play 15 | * use mouse to select the active creature to walk or attack 16 | 17 | # Demo 18 | ![rpg1](https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/master/demo/rpg1.png) 19 | ![rpg2](https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/master/demo/rpg2.png) 20 | -------------------------------------------------------------------------------- /demo/rpg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/demo/rpg1.png -------------------------------------------------------------------------------- /demo/rpg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/demo/rpg2.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pygame as pg 2 | from source.main import main 3 | 4 | if __name__=='__main__': 5 | main() 6 | pg.quit() -------------------------------------------------------------------------------- /resources/graphics/armorman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/armorman.png -------------------------------------------------------------------------------- /resources/graphics/bandit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/bandit.png -------------------------------------------------------------------------------- /resources/graphics/devil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/devil.png -------------------------------------------------------------------------------- /resources/graphics/fireball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/fireball.png -------------------------------------------------------------------------------- /resources/graphics/footman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/footman.png -------------------------------------------------------------------------------- /resources/graphics/magician.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/magician.png -------------------------------------------------------------------------------- /resources/graphics/mouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/mouse.png -------------------------------------------------------------------------------- /resources/graphics/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/resources/graphics/tile.png -------------------------------------------------------------------------------- /source/AStarSearch.py: -------------------------------------------------------------------------------- 1 | __author__ = 'marble_xu' 2 | 3 | from .component import map 4 | from . import tool 5 | 6 | class SearchEntry(): 7 | def __init__(self, x, y, g_cost, f_cost=0, pre_entry=None): 8 | self.x = x 9 | self.y = y 10 | # cost move form start entry to this entry 11 | self.g_cost = g_cost 12 | self.f_cost = f_cost 13 | self.pre_entry = pre_entry 14 | 15 | def getPos(self): 16 | return (self.x, self.y) 17 | 18 | 19 | def AStarSearch(map, source, dest): 20 | def getNewPosition(map, locatioin, offset): 21 | x,y = (location.x + offset[0], location.y + offset[1]) 22 | if not map.isValid(x, y) or not map.isMovable(x, y): 23 | return None 24 | return (x, y) 25 | 26 | def getPositions(map, location): 27 | offsets = tool.getMovePositions(location.x, location.y) 28 | poslist = [] 29 | for offset in offsets: 30 | pos = getNewPosition(map, location, offset) 31 | if pos is not None: 32 | poslist.append(pos) 33 | return poslist 34 | 35 | # imporve the heuristic distance more precisely in future 36 | def calHeuristic(map, pos, dest): 37 | return map.calHeuristicDistance(dest.x, dest.y, pos[0], pos[1]) 38 | 39 | def getMoveCost(location, pos): 40 | if location.x != pos[0] and location.y != pos[1]: 41 | return 1.4 42 | else: 43 | return 1 44 | 45 | # check if the position is in list 46 | def isInList(list, pos): 47 | if pos in list: 48 | return list[pos] 49 | return None 50 | 51 | # add available adjacent positions 52 | def addAdjacentPositions(map, location, dest, openlist, closedlist): 53 | poslist = getPositions(map, location) 54 | for pos in poslist: 55 | # if position is already in closedlist, do nothing 56 | if isInList(closedlist, pos) is None: 57 | findEntry = isInList(openlist, pos) 58 | h_cost = calHeuristic(map, pos, dest) 59 | g_cost = location.g_cost + getMoveCost(location, pos) 60 | if findEntry is None : 61 | # if position is not in openlist, add it to openlist 62 | openlist[pos] = SearchEntry(pos[0], pos[1], g_cost, g_cost+h_cost, location) 63 | elif findEntry.g_cost > g_cost: 64 | # if position is in openlist and cost is larger than current one, 65 | # then update cost and previous position 66 | findEntry.g_cost = g_cost 67 | findEntry.f_cost = g_cost + h_cost 68 | findEntry.pre_entry = location 69 | 70 | # find a least cost position in openlist, return None if openlist is empty 71 | def getFastPosition(openlist): 72 | fast = None 73 | for entry in openlist.values(): 74 | if fast is None: 75 | fast = entry 76 | elif fast.f_cost > entry.f_cost: 77 | fast = entry 78 | return fast 79 | 80 | openlist = {} 81 | closedlist = {} 82 | location = SearchEntry(source[0], source[1], 0.0) 83 | dest = SearchEntry(dest[0], dest[1], 0.0) 84 | openlist[source] = location 85 | 86 | while True: 87 | location = getFastPosition(openlist) 88 | if location is None: 89 | # not found valid path 90 | print("can't find valid path") 91 | break; 92 | 93 | if location.x == dest.x and location.y == dest.y: 94 | break 95 | 96 | closedlist[location.getPos()] = location 97 | openlist.pop(location.getPos()) 98 | addAdjacentPositions(map, location, dest, openlist, closedlist) 99 | 100 | return location 101 | 102 | def getFirstStepAndDistance(location): 103 | distance = 0 104 | tmp = location 105 | while location.pre_entry is not None: 106 | distance += 1 107 | tmp = location 108 | location = location.pre_entry 109 | return (tmp.x, tmp.y, distance) 110 | 111 | def getPosInRange(location, range): 112 | '''get the position which distance from it to destination is range ''' 113 | tmp = location 114 | while location.pre_entry is not None: 115 | if range == 0: 116 | break 117 | location = location.pre_entry 118 | tmp = location 119 | range -= 1 120 | 121 | return (tmp.x, tmp.y) 122 | 123 | def getAStarDistance(map, source, dest): 124 | location = AStarSearch(map, source, dest) 125 | if location is not None: 126 | _, _, distance = getFirstStepAndDistance(location) 127 | else: 128 | distance = None 129 | return distance 130 | -------------------------------------------------------------------------------- /source/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/source/__init__.py -------------------------------------------------------------------------------- /source/component/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/source/component/__init__.py -------------------------------------------------------------------------------- /source/component/entity.py: -------------------------------------------------------------------------------- 1 | __author__ = 'marble_xu' 2 | 3 | import pygame as pg 4 | from .. import tool 5 | from .. import constants as c 6 | from .. import AStarSearch 7 | from . import map 8 | 9 | class FireBall(): 10 | def __init__(self, x, y, enemy, hurt): 11 | # first 3 Frames are flying, last 4 frams are exploding 12 | frame_rect = (0,0,14,14) 13 | self.image = tool.get_image(tool.GFX[c.FIREBALL], *frame_rect, c.BLACK, c.SIZE_MULTIPLIER) 14 | self.rect = self.image.get_rect() 15 | self.rect.centerx = x 16 | self.rect.centery = y 17 | self.enemy = enemy 18 | self.hurt = hurt 19 | self.done = False 20 | self.calVelocity() 21 | 22 | def calVelocity(self): 23 | #print('calVelocity: x:', self.enemy.rect.centerx, self.rect.centerx, 'y:', self.enemy.rect.centery,self.rect.centery) 24 | dis_x = self.enemy.rect.centerx - self.rect.centerx 25 | dis_y = self.enemy.rect.centery - self.rect.centery 26 | distance = (dis_x ** 2 + dis_y ** 2) ** 0.5 27 | self.x_vel = (dis_x * 10)/distance 28 | self.y_vel = (dis_y * 10)/distance 29 | 30 | def update(self): 31 | self.rect.x += self.x_vel 32 | self.rect.y += self.y_vel 33 | if abs(self.rect.x - self.enemy.rect.x) + abs(self.rect.y - self.enemy.rect.y) < 25: 34 | self.enemy.setHurt(self.hurt) 35 | self.done = True 36 | 37 | def draw(self, surface): 38 | surface.blit(self.image, self.rect) 39 | 40 | class EntityAttr(): 41 | def __init__(self, data): 42 | self.max_health = data[c.ATTR_HEALTH] 43 | self.range = data[c.ATTR_RANGE] 44 | self.damage = data[c.ATTR_DAMAGE] 45 | self.attack = data[c.ATTR_ATTACK] 46 | self.defense = data[c.ATTR_DEFENSE] 47 | self.speed = data[c.ATTR_SPEED] 48 | if data[c.ATTR_REMOTE] == 0: 49 | self.remote = False 50 | else: 51 | self.remote = True 52 | 53 | def getHurt(self, enemy_attr): 54 | offset = 0 55 | if self.attack > enemy_attr.defense: 56 | offset = (self.attack - enemy_attr.defense) * 0.05 57 | elif self.attack < enemy_attr.defense: 58 | offset = (self.attack - enemy_attr.defense) * 0.025 59 | hurt = int(self.damage * (1 + offset)) 60 | return hurt 61 | 62 | class Entity(): 63 | def __init__(self, group, sheet, map_x, map_y, data): 64 | self.group = group 65 | self.group_id = group.group_id 66 | self.map_x = map_x 67 | self.map_y = map_y 68 | self.frames = [] 69 | self.frame_index = 0 70 | self.loadFrames(sheet) 71 | self.image = self.frames[self.frame_index] 72 | self.rect = self.image.get_rect() 73 | self.rect.x, self.rect.y = self.getRectPos(map_x, map_y) 74 | 75 | self.attr = EntityAttr(data) 76 | self.health = self.attr.max_health 77 | self.weapon = None 78 | self.enemy = None 79 | self.state = c.IDLE 80 | self.animate_timer = 0.0 81 | self.current_time = 0.0 82 | self.move_speed = c.MOVE_SPEED 83 | 84 | def getRectPos(self, map_x, map_y): 85 | if c.MAP_HEXAGON: 86 | base_x, base_y = tool.getHexMapPos(map_x, map_y) 87 | return (base_x + 4, base_y + 6) 88 | else: 89 | return(map_x * c.REC_SIZE + 5, map_y * c.REC_SIZE + 8) 90 | 91 | def getRecIndex(self, x, y): 92 | if c.MAP_HEXAGON: 93 | x += c.HEX_X_SIZE // 2 - 4 94 | y += c.HEX_Y_SIZE // 2 - 6 95 | map_x, map_y = tool.getHexMapIndex(x, y) 96 | else: 97 | map_x, map_y = (x//c.REC_SIZE, y//c.REC_SIZE) 98 | return (map_x, map_y) 99 | 100 | def loadFrames(self, sheet): 101 | frame_rect_list = [(64, 0, 32, 32), (96, 0, 32, 32)] 102 | for frame_rect in frame_rect_list: 103 | self.frames.append(tool.get_image(sheet, *frame_rect, 104 | c.BLACK, c.SIZE_MULTIPLIER)) 105 | 106 | def setDestination(self, map_x, map_y, enemy=None): 107 | self.dest_x, self.dest_y = self.getRectPos(map_x, map_y) 108 | self.next_x, self.next_y = self.rect.x, self.rect.y 109 | self.enemy = enemy 110 | self.state = c.WALK 111 | 112 | def setTarget(self, enemy): 113 | self.enemy = enemy 114 | self.state = c.ATTACK 115 | 116 | def getHealthRatio(self): 117 | if self.health > 0: 118 | return self.health / self.attr.max_health 119 | else: 120 | return 0 121 | 122 | def isDead(self): 123 | return self.health <= 0 124 | 125 | def isRemote(self): 126 | return self.attr.remote 127 | 128 | def inRange(self, map, map_x, map_y): 129 | location = AStarSearch.AStarSearch(map, (self.map_x, self.map_y), (map_x, map_y)) 130 | if location is not None: 131 | _, _, distance = AStarSearch.getFirstStepAndDistance(location) 132 | if distance <= self.attr.range: 133 | return True 134 | return False 135 | 136 | def putHurt(self, enemy): 137 | hurt = self.attr.getHurt(enemy.attr) 138 | enemy.setHurt(hurt) 139 | 140 | def setHurt(self, damage): 141 | self.health -= damage 142 | if self.isDead(): 143 | self.group.removeEntity(self) 144 | 145 | def shoot(self, enemy): 146 | hurt = self.attr.getHurt(enemy.attr) 147 | self.weapon = FireBall(*self.rect.center, self.enemy, hurt) 148 | 149 | def walkToDestination(self, map): 150 | if self.rect.x == self.next_x and self.rect.y == self.next_y: 151 | source = self.getRecIndex(self.rect.x, self.rect.y) 152 | dest = self.getRecIndex(self.dest_x, self.dest_y) 153 | location = AStarSearch.AStarSearch(map, source, dest) 154 | if location is not None: 155 | map_x, map_y, _ = AStarSearch.getFirstStepAndDistance(location) 156 | self.next_x, self.next_y = self.getRectPos(map_x, map_y) 157 | else: 158 | self.state = c.IDLE 159 | 160 | if c.MAP_HEXAGON and self.rect.x != self.next_x and self.rect.y != self.next_y: 161 | self.rect.x += self.move_speed if self.rect.x < self.next_x else -self.move_speed 162 | self.rect.y += self.move_speed if self.rect.y < self.next_y else -self.move_speed 163 | elif self.rect.x != self.next_x: 164 | self.rect.x += self.move_speed if self.rect.x < self.next_x else -self.move_speed 165 | elif self.rect.y != self.next_y: 166 | self.rect.y += self.move_speed if self.rect.y < self.next_y else -self.move_speed 167 | 168 | def update(self, game_info, map): 169 | self.current_time = game_info[c.CURRENT_TIME] 170 | if self.state == c.WALK: 171 | if (self.current_time - self.animate_timer) > 250: 172 | if self.frame_index == 0: 173 | self.frame_index = 1 174 | else: 175 | self.frame_index = 0 176 | self.animate_timer = self.current_time 177 | 178 | if self.rect.x != self.dest_x or self.rect.y != self.dest_y: 179 | self.walkToDestination(map) 180 | else: 181 | map.setEntity(self.map_x, self.map_y, None) 182 | self.map_x, self.map_y = self.getRecIndex(self.dest_x, self.dest_y) 183 | map.setEntity(self.map_x, self.map_y, self) 184 | if self.enemy is None: 185 | self.state = c.IDLE 186 | else: 187 | self.state = c.ATTACK 188 | elif self.state == c.ATTACK: 189 | if self.attr.remote: 190 | if self.weapon is None: 191 | self.shoot(self.enemy) 192 | else: 193 | self.weapon.update() 194 | if self.weapon.done: 195 | self.weapon = None 196 | self.enemy = None 197 | self.state = c.IDLE 198 | else: 199 | self.putHurt(self.enemy) 200 | self.enemy = None 201 | self.state = c.IDLE 202 | 203 | if self.state == c.IDLE: 204 | self.frame_index = 0 205 | 206 | def draw(self, surface): 207 | self.image = self.frames[self.frame_index] 208 | surface.blit(self.image, self.rect) 209 | width = self.rect.width * self.getHealthRatio() 210 | height = 5 211 | pg.draw.rect(surface, c.RED, pg.Rect(self.rect.left, self.rect.top - height - 1, width, height)) 212 | 213 | if self.weapon is not None: 214 | self.weapon.draw(surface) 215 | 216 | class EntityGroup(): 217 | def __init__(self, group_id): 218 | self.group = [] 219 | self.group_id = group_id 220 | self.entity_index = 0 221 | 222 | def createEntity(self, entity_list, map): 223 | for data in entity_list: 224 | entity_name, map_x, map_y = data['name'], data['x'], data['y'] 225 | if map_x < 0: 226 | map_x = c.GRID_X_LEN + map_x 227 | if map_y < 0: 228 | map_y = c.GRID_Y_LEN + map_y 229 | 230 | entity = Entity(self, tool.GFX[entity_name], map_x, map_y, tool.ATTR[entity_name]) 231 | self.group.append(entity) 232 | map.setEntity(map_x, map_y, entity) 233 | 234 | #self.group = sorted(self.group, key=lambda x:x.attr.speed, reverse=True) 235 | 236 | def removeEntity(self, entity): 237 | for i in range(len(self.group)): 238 | if self.group[i] == entity: 239 | if (self.entity_index > i or 240 | (self.entity_index >= len(self.group) - 1)): 241 | self.entity_index -= 1 242 | self.group.remove(entity) 243 | 244 | def isEmpty(self): 245 | if len(self.group) == 0: 246 | return True 247 | return False 248 | 249 | def nextTurn(self): 250 | self.entity_index = 0 251 | 252 | def getActiveEntity(self): 253 | if self.entity_index >= len(self.group): 254 | entity = None 255 | else: 256 | entity = self.group[self.entity_index] 257 | return entity 258 | 259 | def consumeEntity(self): 260 | self.entity_index += 1 261 | 262 | def update(self, game_info, map): 263 | for entity in self.group: 264 | entity.update(game_info, map) 265 | 266 | def draw(self, surface): 267 | for entity in self.group: 268 | entity.draw(surface) -------------------------------------------------------------------------------- /source/component/map.py: -------------------------------------------------------------------------------- 1 | __author__ = 'marble_xu' 2 | 3 | import pygame as pg 4 | from .. import tool 5 | from .. import constants as c 6 | 7 | class Map(): 8 | def __init__(self, width, height, grid): 9 | self.width = width 10 | self.height = height 11 | self.bg_map = [[0 for x in range(self.width)] for y in range(self.height)] 12 | self.entity_map = [[None for x in range(self.width)] for y in range(self.height)] 13 | self.active_entity = None 14 | self.select = None 15 | self.setupMapImage(grid) 16 | self.setupMouseImage() 17 | 18 | def setupMapImage(self, grid): 19 | self.grid_map = [[0 for x in range(self.width)] for y in range(self.height)] 20 | if grid is not None: 21 | for data in grid: 22 | x, y, type = data['x'], data['y'], data['type'] 23 | self.grid_map[y][x] = type 24 | 25 | self.map_image = pg.Surface((self.width * c.REC_SIZE, self.height * c.REC_SIZE)).convert() 26 | self.rect = self.map_image.get_rect() 27 | self.rect.x = 0 28 | self.rect.y = 0 29 | for y in range(self.height): 30 | for x in range(self.width): 31 | type = self.grid_map[y][x] 32 | if type != c.MAP_EMPTY: 33 | if c.MAP_HEXAGON: 34 | base_x, base_y = tool.getHexMapPos(x, y) 35 | self.map_image.blit(tool.GRID[type], (base_x, base_y)) 36 | else: 37 | self.map_image.blit(tool.GRID[type], (x * c.REC_SIZE, y * c.REC_SIZE)) 38 | self.map_image.set_colorkey(c.BLACK) 39 | 40 | def setupMouseImage(self): 41 | self.mouse_frames = [] 42 | frame_rect = (0, 0, 25, 27) 43 | self.mouse_image = tool.get_image(tool.GFX[c.MOUSE], *frame_rect, c.BLACK, 1) 44 | self.mouse_rect = self.mouse_image.get_rect() 45 | pg.mouse.set_visible(False) 46 | 47 | def isValid(self, map_x, map_y): 48 | if c.MAP_HEXAGON: 49 | if map_y % 2 == 0: 50 | max_x = self.width 51 | else: 52 | max_x = self.width - 1 53 | else: 54 | max_x = self.width 55 | if (map_x < 0 or map_x >= max_x or 56 | map_y < 0 or map_y >= self.height): 57 | return False 58 | return True 59 | 60 | def isMovable(self, map_x, map_y): 61 | return (self.entity_map[map_y][map_x] == None and 62 | self.grid_map[map_y][map_x] != c.MAP_STONE) 63 | 64 | def getMapIndex(self, x, y): 65 | if c.MAP_HEXAGON: 66 | return tool.getHexMapIndex(x, y) 67 | else: 68 | return (x//c.REC_SIZE, y//c.REC_SIZE) 69 | 70 | def getDistance(self, x1, y1, map_x2, map_y2): 71 | if c.MAP_HEXAGON: 72 | x2, y2 = tool.getHexMapPos(map_x2, map_y2) 73 | x2 += c.HEX_X_SIZE // 2 74 | y2 += c.HEX_Y_SIZE // 2 75 | distance = (abs(x1 - x2) + abs(y1 - y2)) 76 | else: 77 | map_x1, map_y1 = self.getMapIndex(x1, y1) 78 | x2 = map_x2 * c.REC_SIZE + c.REC_SIZE//2 79 | y2 = map_y2 * c.REC_SIZE + c.REC_SIZE//2 80 | distance = (abs(x1 - x2) + abs(y1 - y2)) 81 | if map_x1 != map_x2 and map_y1 != map_y2: 82 | distance -= c.REC_SIZE//2 83 | return distance 84 | 85 | def checkMouseClick(self, x, y): 86 | if self.active_entity is None: 87 | return False 88 | 89 | map_x, map_y = self.getMapIndex(x, y) 90 | if not self.isValid(map_x, map_y): 91 | return False 92 | 93 | entity = self.entity_map[map_y][map_x] 94 | if ((entity is None or entity == self.active_entity) and 95 | self.active_entity.inRange(self, map_x, map_y)): 96 | self.active_entity.setDestination(map_x, map_y) 97 | return True 98 | elif entity is not None: 99 | if self.active_entity.isRemote(): 100 | self.active_entity.setTarget(entity) 101 | return True 102 | elif self.select is not None: 103 | self.active_entity.setDestination(self.select[0], self.select[1], entity) 104 | return True 105 | return False 106 | 107 | def checkMouseMove(self, x, y): 108 | if self.active_entity is None: 109 | return False 110 | 111 | map_x, map_y = self.getMapIndex(x, y) 112 | if not self.isValid(map_x, map_y): 113 | return False 114 | 115 | self.select = None 116 | entity = self.entity_map[map_y][map_x] 117 | if ((self.isMovable(map_x, map_y) or entity == self.active_entity) and 118 | self.active_entity.inRange(self, map_x, map_y)): 119 | self.bg_map[map_y][map_x] = c.BG_SELECT 120 | elif entity is not None: 121 | if entity.group_id != self.active_entity.group_id: 122 | if self.active_entity.isRemote(): 123 | self.bg_map[map_y][map_x] = c.BG_ATTACK 124 | else: 125 | dir_list = tool.getAttackPositions(map_x, map_y) 126 | res_list = [] 127 | for offset_x, offset_y in dir_list: 128 | if self.isValid(map_x + offset_x, map_y + offset_y): 129 | type = self.bg_map[map_y + offset_y][map_x + offset_x] 130 | if type == c.BG_RANGE or type == c.BG_ACTIVE: 131 | res_list.append((map_x + offset_x, map_y + offset_y)) 132 | if len(res_list) > 0: 133 | min_dis = c.MAP_WIDTH 134 | for tmp_x, tmp_y in res_list: 135 | distance = self.getDistance(x, y, tmp_x, tmp_y) 136 | if distance < min_dis: 137 | min_dis = distance 138 | res = (tmp_x, tmp_y) 139 | self.bg_map[res[1]][res[0]] = c.BG_SELECT 140 | self.bg_map[map_y][map_x] = c.BG_ATTACK 141 | self.select = res 142 | 143 | def setEntity(self, map_x, map_y, value): 144 | self.entity_map[map_y][map_x] = value 145 | 146 | def drawMouseShow(self, surface): 147 | x, y = pg.mouse.get_pos() 148 | map_x, map_y = self.getMapIndex(x, y) 149 | if self.isValid(map_x, map_y): 150 | self.mouse_rect.x = x 151 | self.mouse_rect.y = y 152 | surface.blit(self.mouse_image, self.mouse_rect) 153 | 154 | def updateMap(self): 155 | for y in range(self.height): 156 | for x in range(self.width): 157 | self.bg_map[y][x] = c.BG_EMPTY 158 | if self.entity_map[y][x] is not None and self.entity_map[y][x].isDead(): 159 | self.entity_map[y][x] = None 160 | 161 | if self.active_entity is None or self.active_entity.state != c.IDLE: 162 | return 163 | map_x, map_y = self.active_entity.map_x, self.active_entity.map_y 164 | self.bg_map[map_y][map_x] = c.BG_ACTIVE 165 | 166 | for y in range(self.height): 167 | for x in range(self.width): 168 | if not self.isMovable(x,y) or not self.isValid(x,y): 169 | continue 170 | if self.active_entity.inRange(self, x, y): 171 | self.bg_map[y][x] = c.BG_RANGE 172 | mouse_x, mouse_y = pg.mouse.get_pos() 173 | self.checkMouseMove(mouse_x, mouse_y) 174 | 175 | def drawBackground(self, surface): 176 | if c.MAP_HEXAGON: 177 | return self.drawBackgroundHex(surface) 178 | 179 | pg.draw.rect(surface, c.LIGHTYELLOW, pg.Rect(0, 0, c.MAP_WIDTH, c.MAP_HEIGHT)) 180 | 181 | for y in range(self.height): 182 | for x in range(self.width): 183 | if self.bg_map[y][x] == c.BG_EMPTY: 184 | color = c.LIGHTYELLOW 185 | elif self.bg_map[y][x] == c.BG_ACTIVE: 186 | color = c.SKY_BLUE 187 | elif self.bg_map[y][x] == c.BG_RANGE: 188 | color = c.NAVYBLUE 189 | elif self.bg_map[y][x] == c.BG_SELECT: 190 | color = c.GREEN 191 | elif self.bg_map[y][x] == c.BG_ATTACK: 192 | color = c.GOLD 193 | pg.draw.rect(surface, color, (x * c.REC_SIZE, y * c.REC_SIZE, 194 | c.REC_SIZE, c.REC_SIZE)) 195 | 196 | surface.blit(self.map_image, self.rect) 197 | 198 | for y in range(self.height): 199 | # draw a horizontal line 200 | start_pos = (0, 0 + c.REC_SIZE * y) 201 | end_pos = (c.MAP_WIDTH, c.REC_SIZE * y) 202 | pg.draw.line(surface, c.BLACK, start_pos, end_pos, 1) 203 | 204 | for x in range(self.width): 205 | # draw a horizontal line 206 | start_pos = (c.REC_SIZE * x, 0) 207 | end_pos = (c.REC_SIZE * x, c.MAP_HEIGHT) 208 | pg.draw.line(surface, c.BLACK, start_pos, end_pos, 1) 209 | 210 | def calHeuristicDistance(self, x1, y1, x2, y2): 211 | if c.MAP_HEXAGON: 212 | dis_y = abs(y1 - y2) 213 | dis_x = abs(x1 - x2) 214 | half_y = dis_y // 2 215 | if dis_y >= dis_x: 216 | dis_x = 0 217 | else: 218 | dis_x -= half_y 219 | return (dis_y + dis_x) 220 | else: 221 | return abs(x1 - x2) + abs(y1 - y2) 222 | 223 | def drawBackgroundHex(self, surface): 224 | Y_LEN = c.HEX_Y_SIZE // 2 225 | X_LEN = c.HEX_X_SIZE // 2 226 | 227 | pg.draw.rect(surface, c.LIGHTYELLOW, pg.Rect(0, 0, c.MAP_WIDTH, c.MAP_HEIGHT)) 228 | 229 | for y in range(self.height): 230 | for x in range(self.width): 231 | if self.bg_map[y][x] == c.BG_EMPTY: 232 | color = c.LIGHTYELLOW 233 | elif self.bg_map[y][x] == c.BG_ACTIVE: 234 | color = c.SKY_BLUE 235 | elif self.bg_map[y][x] == c.BG_RANGE: 236 | color = c.NAVYBLUE 237 | elif self.bg_map[y][x] == c.BG_SELECT: 238 | color = c.GREEN 239 | elif self.bg_map[y][x] == c.BG_ATTACK: 240 | color = c.GOLD 241 | 242 | base_x, base_y = tool.getHexMapPos(x, y) 243 | points = [(base_x, base_y + Y_LEN//2 + Y_LEN), (base_x, base_y + Y_LEN//2), 244 | (base_x + X_LEN, base_y), (base_x + X_LEN * 2, base_y + Y_LEN//2), 245 | (base_x + X_LEN * 2, base_y + Y_LEN//2 + Y_LEN), (base_x + X_LEN, base_y + Y_LEN*2)] 246 | pg.draw.polygon(surface, color, points) 247 | 248 | surface.blit(self.map_image, self.rect) 249 | 250 | for y in range(self.height): 251 | for x in range(self.width): 252 | if y % 2 == 1 and x == self.width - 1: 253 | continue 254 | base_x, base_y = tool.getHexMapPos(x, y) 255 | points = [(base_x, base_y + Y_LEN//2 + Y_LEN), (base_x, base_y + Y_LEN//2), 256 | (base_x + X_LEN, base_y), (base_x + X_LEN * 2, base_y + Y_LEN//2), 257 | (base_x + X_LEN * 2, base_y + Y_LEN//2 + Y_LEN), (base_x + X_LEN, base_y + Y_LEN*2)] 258 | pg.draw.lines(surface, c.BLACK, True, points) -------------------------------------------------------------------------------- /source/constants.py: -------------------------------------------------------------------------------- 1 | 2 | MAP_HEXAGON = False 3 | 4 | ORIGINAL_CAPTION = 'RPG Game' 5 | 6 | GRID_X_LEN = 10 7 | GRID_Y_LEN = 12 8 | 9 | if MAP_HEXAGON: 10 | REC_SIZE = 56 11 | HEX_Y_SIZE = 56 12 | HEX_X_SIZE = 48 13 | MAP_WIDTH = GRID_X_LEN * HEX_X_SIZE + HEX_X_SIZE//4 14 | MAP_HEIGHT = GRID_Y_LEN//2 * (HEX_Y_SIZE//2) * 3 + HEX_Y_SIZE//4 15 | else: 16 | REC_SIZE = 50 17 | MAP_WIDTH = GRID_X_LEN * REC_SIZE 18 | MAP_HEIGHT = GRID_Y_LEN * REC_SIZE 19 | 20 | 21 | SCREEN_WIDTH = MAP_WIDTH 22 | SCREEN_HEIGHT = MAP_HEIGHT 23 | SCREEN_SIZE = (SCREEN_WIDTH, SCREEN_HEIGHT) 24 | 25 | 26 | WHITE = (255, 255, 255) 27 | NAVYBLUE = ( 60, 60, 100) 28 | SKY_BLUE = ( 39, 145, 251) 29 | BLACK = ( 0, 0, 0) 30 | LIGHTYELLOW = (247, 238, 214) 31 | RED = (255, 0, 0) 32 | PURPLE = (255, 0, 255) 33 | GOLD = (255, 215, 0) 34 | GREEN = ( 0, 255, 0) 35 | 36 | SIZE_MULTIPLIER = 1.3 37 | 38 | #GAME INFO DICTIONARY KEYS 39 | CURRENT_TIME = 'current time' 40 | LEVEL_NUM = 'level num' 41 | 42 | #STATES FOR ENTIRE GAME 43 | MAIN_MENU = 'main menu' 44 | LOAD_SCREEN = 'load screen' 45 | GAME_OVER = 'game over' 46 | LEVEL = 'level' 47 | 48 | #MAP BACKGROUND STATE 49 | BG_EMPTY = 0 50 | BG_ACTIVE = 1 51 | BG_RANGE = 2 52 | BG_SELECT = 3 53 | BG_ATTACK = 4 54 | 55 | #MAP GRID TYPE 56 | MAP_EMPTY = 0 57 | MAP_STONE = 1 58 | MAP_GRASS = 2 59 | MAP_DESERT = 3 60 | MAP_LAKE = 4 61 | 62 | GROUP1 = 'group1' 63 | GROUP2 = 'group2' 64 | MAP_GRID = 'mapgrid' 65 | 66 | #Entity State 67 | IDLE = 'idle' 68 | WALK = 'walk' 69 | ATTACK = 'attack' 70 | 71 | #Entity Attribute 72 | ATTR_HEALTH = 'health' 73 | ATTR_RANGE = 'range' 74 | ATTR_DAMAGE = 'damage' 75 | ATTR_ATTACK = 'attack' 76 | ATTR_DEFENSE = 'defense' 77 | ATTR_REMOTE = 'remote' # remote army or melee army 78 | ATTR_SPEED = 'speed' # higher speed army can act prior to lower speed army in a game turn 79 | 80 | #Entity Name 81 | DEVIL = 'devil' 82 | SOLDIER = 'footman' 83 | MAGICIAN = 'magician' 84 | FIREBALL = 'fireball' 85 | MOUSE = 'mouse' 86 | 87 | #Game State 88 | INIT = 'init' 89 | SELECT = 'select' 90 | ENTITY_ACT = 'entity act' 91 | 92 | #Game Setting 93 | MOVE_SPEED = 2 94 | 95 | 96 | -------------------------------------------------------------------------------- /source/data/entity.json: -------------------------------------------------------------------------------- 1 | { 2 | "devil":{ 3 | "health":100, 4 | "range":5, 5 | "damage":40, 6 | "attack":5, 7 | "defense":5, 8 | "remote":0, 9 | "speed":6 10 | }, 11 | "bandit":{ 12 | "health":100, 13 | "range":6, 14 | "damage":40, 15 | "attack":7, 16 | "defense":4, 17 | "remote":0, 18 | "speed":6 19 | }, 20 | "footman":{ 21 | "health":100, 22 | "range":5, 23 | "damage":40, 24 | "attack":5, 25 | "defense":5, 26 | "remote":0, 27 | "speed":5 28 | }, 29 | "armorman":{ 30 | "health":100, 31 | "range":6, 32 | "damage":50, 33 | "attack":6, 34 | "defense":4, 35 | "remote":0, 36 | "speed":5 37 | }, 38 | "magician":{ 39 | "health":70, 40 | "range":4, 41 | "damage":60, 42 | "attack":10, 43 | "defense":2, 44 | "remote":1, 45 | "speed":4 46 | } 47 | } -------------------------------------------------------------------------------- /source/data/map/level_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "mapgrid":[ 3 | {"x": 3, "y": 7, "type":1}, 4 | {"x": 5, "y": 7, "type":1}, 5 | {"x": 5, "y": 5, "type":2} 6 | ], 7 | "group1":[ 8 | {"name":"devil", "x": 1, "y": 0}, 9 | {"name":"devil", "x": 4, "y": 0}, 10 | {"name":"devil", "x": 7, "y": 0} 11 | ], 12 | "group2":[ 13 | {"name":"footman", "x": 2, "y": -1}, 14 | {"name":"magician", "x": 4, "y": -1}, 15 | {"name":"footman", "x": 6, "y": -1} 16 | ] 17 | } -------------------------------------------------------------------------------- /source/data/map/level_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "mapgrid":[ 3 | {"x": 3, "y": 7, "type":1}, 4 | {"x": 5, "y": 7, "type":1}, 5 | {"x": 5, "y": 5, "type":2} 6 | ], 7 | "group1":[ 8 | {"name":"devil", "x": 1, "y": 0}, 9 | {"name":"devil", "x": 3, "y": 0}, 10 | {"name":"devil", "x": 5, "y": 0}, 11 | {"name":"devil", "x": 7, "y": 0} 12 | ], 13 | "group2":[ 14 | {"name":"footman", "x": 2, "y": -1}, 15 | {"name":"magician", "x": 4, "y": -1}, 16 | {"name":"footman", "x": 6, "y": -1} 17 | ] 18 | } -------------------------------------------------------------------------------- /source/data/map/level_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "mapgrid":[ 3 | {"x": 1, "y": 9, "type":1}, 4 | {"x": 2, "y": 9, "type":1}, 5 | {"x": 3, "y": 9, "type":1}, 6 | {"x": 5, "y": 5, "type":2} 7 | ], 8 | "group1":[ 9 | {"name":"devil", "x": 1, "y": 0}, 10 | {"name":"devil", "x": 3, "y": 0}, 11 | {"name":"devil", "x": 5, "y": 0}, 12 | {"name":"devil", "x": 7, "y": 0}, 13 | {"name":"devil", "x": 9, "y": 0} 14 | ], 15 | "group2":[ 16 | {"name":"footman", "x": 0, "y": -1}, 17 | {"name":"magician", "x": 2, "y": -1}, 18 | {"name":"footman", "x": 4, "y": -1} 19 | ] 20 | } -------------------------------------------------------------------------------- /source/data/map/level_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "mapgrid":[ 3 | {"x": 3, "y": 8, "type":1}, 4 | {"x": 5, "y": 8, "type":1}, 5 | {"x": 5, "y": 5, "type":2} 6 | ], 7 | "group1":[ 8 | {"name":"devil", "x": 2, "y": 0}, 9 | {"name":"devil", "x": 4, "y": 0}, 10 | {"name":"devil", "x": 6, "y": 0} 11 | ], 12 | "group2":[ 13 | {"name":"magician", "x": 2, "y": -1}, 14 | {"name":"footman", "x": 4, "y": -1} 15 | ] 16 | } -------------------------------------------------------------------------------- /source/data/map/level_5.json: -------------------------------------------------------------------------------- 1 | { 2 | "mapgrid":[ 3 | {"x": 3, "y": 7, "type":1}, 4 | {"x": 5, "y": 7, "type":1}, 5 | {"x": 5, "y": 5, "type":2} 6 | ], 7 | "group1":[ 8 | {"name":"bandit", "x": 1, "y": 0}, 9 | {"name":"bandit", "x": 3, "y": 0}, 10 | {"name":"bandit", "x": 5, "y": 0}, 11 | {"name":"bandit", "x": 7, "y": 0} 12 | ], 13 | "group2":[ 14 | {"name":"footman", "x": 2, "y": -1}, 15 | {"name":"magician", "x": 4, "y": -1}, 16 | {"name":"footman", "x": 6, "y": -1} 17 | ] 18 | } -------------------------------------------------------------------------------- /source/gameAI.py: -------------------------------------------------------------------------------- 1 | __author__ = 'marble_xu' 2 | 3 | from .component import map 4 | from . import AStarSearch, tool 5 | 6 | class EnemyInfo(): 7 | def __init__(self, enemy): 8 | self.enemy = enemy 9 | self.range_num = 100 10 | self.kill_time = 1000 11 | self.remote = False 12 | 13 | def getAction(entity, map, enemy_group): 14 | def getDestination(entity, map, enemy): 15 | dir_list = tool.getAttackPositions(enemy.map_x, enemy.map_y) 16 | best_pos = None 17 | min_dis = 0 18 | for offset_x, offset_y in dir_list: 19 | x, y = enemy.map_x + offset_x, enemy.map_y + offset_y 20 | if map.isValid(x, y) and map.isMovable(x, y): 21 | distance = AStarSearch.getAStarDistance(map, (entity.map_x, entity.map_y), (x, y)) 22 | if distance is None: 23 | continue 24 | if best_pos is None: 25 | best_pos = (x, y) 26 | min_dis = distance 27 | elif distance < min_dis: 28 | best_pos = (x, y) 29 | min_dis = distance 30 | return best_pos 31 | 32 | def getEnemyInfo(info_list, entity, enemy, destination): 33 | enemyinfo = EnemyInfo(enemy) 34 | location = AStarSearch.AStarSearch(map, (entity.map_x, entity.map_y), destination) 35 | _, _, distance = AStarSearch.getFirstStepAndDistance(location) 36 | 37 | print('entity(%d, %d), dest(%d, %d) location(%d, %d) distance:%d' % 38 | (entity.map_x, entity.map_y, destination[0], destination[1], location.x, location.y, distance)) 39 | 40 | if distance == 0: 41 | enemyinfo.range_num = 0 42 | else: 43 | enemyinfo.range_num = (distance - 1) // entity.attr.range 44 | enemyinfo.location = location 45 | enemyinfo.distance = distance 46 | 47 | hurt = entity.attr.getHurt(enemy.attr) 48 | enemyinfo.kill_time = (enemy.health-1) // hurt 49 | 50 | enemyinfo.remote = enemy.attr.remote 51 | info_list.append(enemyinfo) 52 | 53 | info_list = [] 54 | best_info = None 55 | for enemy in enemy_group: 56 | if tool.isNextToEntity(entity, enemy): 57 | print('entity(%d,%d) next to enemy(%d, %d)' % (entity.map_x, entity.map_y, enemy.map_x, enemy.map_y)) 58 | destination = (entity.map_x, entity.map_y) 59 | else: 60 | destination = getDestination(entity, map, enemy) 61 | if destination is not None: 62 | getEnemyInfo(info_list, entity, enemy, destination) 63 | 64 | for info in info_list: 65 | if best_info == None: 66 | best_info = info 67 | else: 68 | if info.range_num < best_info.range_num: 69 | best_info = info 70 | elif info.range_num == best_info.range_num: 71 | if info.range_num == 0: 72 | if info.kill_time < best_info.kill_time: 73 | best_info = info 74 | elif info.kill_time == best_info.kill_time: 75 | if info.remote == True and best_info.remote == False: 76 | best_info = info 77 | elif info.distance < best_info.distance: 78 | best_info = info 79 | else: 80 | if info.distance < best_info.distance: 81 | best_info = info 82 | 83 | if best_info.range_num == 0: 84 | return (best_info.location.x, best_info.location.y, best_info.enemy) 85 | elif best_info.range_num == 1: 86 | range = entity.attr.range 87 | x, y = AStarSearch.getPosInRange(best_info.location, range) 88 | return (x, y, None) 89 | else: 90 | range = best_info.distance - entity.attr.range 91 | x, y = AStarSearch.getPosInRange(best_info.location, range) 92 | return (x, y, None) -------------------------------------------------------------------------------- /source/main.py: -------------------------------------------------------------------------------- 1 | __author__ = 'marble_xu' 2 | 3 | from . import tool 4 | from . import constants as c 5 | from .state import level 6 | 7 | def main(): 8 | game = tool.Control() 9 | state_dict = {c.LEVEL: level.Level()} 10 | game.setup_states(state_dict, c.LEVEL) 11 | game.main() -------------------------------------------------------------------------------- /source/state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblexu/PythonStrategyRPG/613ce1e5d7fe127f5f05bd1b5a05809dbf7a3182/source/state/__init__.py -------------------------------------------------------------------------------- /source/state/level.py: -------------------------------------------------------------------------------- 1 | __author__ = 'marble_xu' 2 | 3 | import os 4 | import json 5 | import pygame as pg 6 | from .. import tool 7 | from .. import constants as c 8 | from .. import AStarSearch 9 | from .. import gameAI 10 | from ..component import map, entity 11 | 12 | 13 | class Level(tool.State): 14 | def __init__(self): 15 | tool.State.__init__(self) 16 | 17 | def startup(self, current_time, persist): 18 | self.game_info = persist 19 | self.persist = self.game_info 20 | self.game_info[c.CURRENT_TIME] = current_time 21 | 22 | self.loadMap() 23 | grid = self.map_data[c.MAP_GRID] if c.MAP_GRID in self.map_data else None 24 | self.map = map.Map(c.GRID_X_LEN, c.GRID_Y_LEN, grid) 25 | self.setupGroup() 26 | self.state = c.IDLE 27 | 28 | def loadMap(self): 29 | map_file = 'level_' + str(self.game_info[c.LEVEL_NUM]) + '.json' 30 | file_path = os.path.join('source', 'data', 'map', map_file) 31 | f = open(file_path) 32 | self.map_data = json.load(f) 33 | f.close() 34 | 35 | def setupGroup(self): 36 | self.group1 = entity.EntityGroup(0) 37 | self.group1.createEntity(self.map_data[c.GROUP1], self.map) 38 | self.group2 = entity.EntityGroup(1) 39 | self.group2.createEntity(self.map_data[c.GROUP2], self.map) 40 | 41 | def getActiveEntity(self): 42 | entity1 = self.group1.getActiveEntity() 43 | entity2 = self.group2.getActiveEntity() 44 | if entity1 and entity2: 45 | if entity1.attr.speed >= entity2.attr.speed: 46 | entity, group = entity1, self.group1 47 | else: 48 | entity, group = entity2, self.group2 49 | elif entity1: 50 | entity, group = entity1, self.group1 51 | elif entity2: 52 | entity, group = entity2, self.group2 53 | else: 54 | return None 55 | return (entity, group) 56 | 57 | def update(self, surface, current_time, mouse_pos): 58 | self.current_time = self.game_info[c.CURRENT_TIME] = current_time 59 | if self.state == c.IDLE: 60 | if self.group1.isEmpty() or self.group2.isEmpty(): 61 | self.done = True 62 | if self.group1.isEmpty(): 63 | self.game_info[c.LEVEL_NUM] += 1 64 | self.next = c.LEVEL 65 | print('Group 1 win!') 66 | else: 67 | print('Group 0 win!') 68 | else: 69 | result = self.getActiveEntity() 70 | if result is not None: 71 | entity, group = result 72 | self.map.active_entity = entity 73 | group.consumeEntity() 74 | self.state = c.SELECT 75 | else: 76 | self.group1.nextTurn() 77 | self.group2.nextTurn() 78 | elif self.state == c.SELECT: 79 | if self.map.active_entity.group_id == 0: 80 | (map_x, map_y, enemy) = gameAI.getAction(self.map.active_entity, self.map, self.group2.group) 81 | print('pos(%d, %d)' % (map_x, map_y)) 82 | self.map.active_entity.setDestination(map_x, map_y, enemy) 83 | self.state = c.ENTITY_ACT 84 | else: 85 | self.map.updateMap() 86 | if mouse_pos is not None: 87 | self.mouseClick(mouse_pos[0], mouse_pos[1]) 88 | elif self.state == c.ENTITY_ACT: 89 | self.map.updateMap() 90 | self.group1.update(self.game_info, self.map) 91 | self.group2.update(self.game_info, self.map) 92 | if self.map.active_entity.state == c.IDLE: 93 | self.state = c.IDLE 94 | self.draw(surface) 95 | 96 | def mouseClick(self, mouse_x, mouse_y): 97 | if self.state == c.SELECT: 98 | if self.map.checkMouseClick(mouse_x, mouse_y): 99 | self.state = c.ENTITY_ACT 100 | 101 | def draw(self, surface): 102 | self.map.drawBackground(surface) 103 | self.group1.draw(surface) 104 | self.group2.draw(surface) 105 | self.map.drawMouseShow(surface) -------------------------------------------------------------------------------- /source/tool.py: -------------------------------------------------------------------------------- 1 | __author__ = 'marble_xu' 2 | 3 | import os 4 | import json 5 | from abc import abstractmethod 6 | import pygame as pg 7 | from . import constants as c 8 | 9 | class State(): 10 | def __init__(self): 11 | self.start_time = 0.0 12 | self.current_time = 0.0 13 | self.done = False 14 | self.next = None 15 | self.persist = {} 16 | 17 | @abstractmethod 18 | def startup(self, current_time, persist): 19 | '''abstract method''' 20 | 21 | def cleanup(self): 22 | self.done = False 23 | return self.persist 24 | 25 | @abstractmethod 26 | def update(sefl, surface, keys, current_time): 27 | '''abstract method''' 28 | 29 | class Control(): 30 | def __init__(self): 31 | self.screen = pg.display.get_surface() 32 | self.done = False 33 | self.clock = pg.time.Clock() 34 | self.fps = 60 35 | self.keys = pg.key.get_pressed() 36 | self.mouse_pos = None 37 | self.current_time = 0.0 38 | self.state_dict = {} 39 | self.state_name = None 40 | self.state = None 41 | self.game_info = {c.CURRENT_TIME:0.0, 42 | c.LEVEL_NUM:1} 43 | 44 | def setup_states(self, state_dict, start_state): 45 | self.state_dict = state_dict 46 | self.state_name = start_state 47 | self.state = self.state_dict[self.state_name] 48 | self.state.startup(self.current_time, self.game_info) 49 | 50 | def update(self): 51 | self.current_time = pg.time.get_ticks() 52 | if self.state.done: 53 | self.flip_state() 54 | self.state.update(self.screen, self.current_time, self.mouse_pos) 55 | self.mouse_pos = None 56 | 57 | def flip_state(self): 58 | previous, self.state_name = self.state_name, self.state.next 59 | persist = self.state.cleanup() 60 | self.state = self.state_dict[self.state_name] 61 | self.state.startup(self.current_time, persist) 62 | 63 | def event_loop(self): 64 | for event in pg.event.get(): 65 | if event.type == pg.QUIT: 66 | self.done = True 67 | elif event.type == pg.KEYDOWN: 68 | self.keys = pg.key.get_pressed() 69 | elif event.type == pg.KEYUP: 70 | self.keys = pg.key.get_pressed() 71 | elif event.type == pg.MOUSEBUTTONDOWN: 72 | self.mouse_pos = pg.mouse.get_pos() 73 | 74 | def main(self): 75 | while not self.done: 76 | self.event_loop() 77 | self.update() 78 | pg.display.update() 79 | self.clock.tick(self.fps) 80 | print('game over') 81 | 82 | def get_image(sheet, x, y, width, height, colorkey, scale): 83 | image = pg.Surface([width, height]) 84 | rect = image.get_rect() 85 | 86 | image.blit(sheet, (0, 0), (x, y, width, height)) 87 | image.set_colorkey(colorkey) 88 | image = pg.transform.scale(image, 89 | (int(rect.width*scale), 90 | int(rect.height*scale))) 91 | return image 92 | 93 | def load_all_gfx(directory, colorkey=c.WHITE, accept=('.png', '.jpg', '.bmp', '.gif')): 94 | graphics = {} 95 | for pic in os.listdir(directory): 96 | name, ext = os.path.splitext(pic) 97 | if ext.lower() in accept: 98 | img = pg.image.load(os.path.join(directory, pic)) 99 | if img.get_alpha(): 100 | img = img.convert_alpha() 101 | else: 102 | img = img.convert() 103 | img.set_colorkey(colorkey) 104 | graphics[name] = img 105 | return graphics 106 | 107 | def load_map_grid_image(): 108 | grid_images = {} 109 | image_rect_dict = {c.MAP_STONE:(80, 48, 16, 16), c.MAP_GRASS:(80, 32, 16, 16)} 110 | for type, rect in image_rect_dict.items(): 111 | grid_images[type] = get_image(GFX['tile'], *rect, c.BLACK, 3) 112 | return grid_images 113 | 114 | def load_entiry_attr(file_path): 115 | attrs = {} 116 | f = open(file_path) 117 | data = json.load(f) 118 | f.close() 119 | for name, attr in data.items(): 120 | attrs[name] = attr 121 | return attrs 122 | 123 | def getMovePositions(x, y): 124 | if c.MAP_HEXAGON: 125 | if y % 2 == 0: 126 | offsets = [(-1, 0), (-1, -1), (0, -1), (1, 0), (-1, 1), (0, 1)] 127 | else: 128 | offsets = [(-1, 0), (0, -1), (1, -1), (1, 0), (0, 1), (1, 1)] 129 | else: 130 | # use four ways or eight ways to move 131 | offsets = [(-1,0), (0, -1), (1, 0), (0, 1)] 132 | #offsets = [(-1,0), (0, -1), (1, 0), (0, 1), (-1,-1), (1, -1), (-1, 1), (1, 1)] 133 | return offsets 134 | 135 | def getAttackPositions(x, y): 136 | if c.MAP_HEXAGON: 137 | return getMovePositions(x, y) 138 | else: 139 | return [(-1,-1), (-1,0), (-1,1), (0,-1), (0,1), (1,-1),(1,0), (1,1)] 140 | 141 | def isNextToEntity(entity1, entity2): 142 | if c.MAP_HEXAGON: 143 | dir_list = getMovePositions(entity1.map_x, entity1.map_y) 144 | for offset_x, offset_y in dir_list: 145 | x, y = entity1.map_x + offset_x, entity1.map_y + offset_y 146 | if x == entity2.map_x and y == entity2.map_y: 147 | return True 148 | else: 149 | if abs(entity1.map_x - entity2.map_x) <= 1 and abs(entity1.map_y - entity2.map_y) <= 1: 150 | return True 151 | return False 152 | 153 | def getHexMapPos(x, y): 154 | X_LEN = c.HEX_X_SIZE // 2 155 | Y_LEN = c.HEX_Y_SIZE // 2 156 | if y % 2 == 0: 157 | base_x = X_LEN * 2 * x 158 | base_y = Y_LEN * 3 * (y//2) 159 | else: 160 | base_x = X_LEN * 2 * x + X_LEN 161 | base_y = Y_LEN * 3 * (y//2) + Y_LEN//2 + Y_LEN 162 | return (base_x, base_y) 163 | 164 | class Vector2d(): 165 | def __init__(self, x, y): 166 | self.x = x 167 | self.y = y 168 | 169 | def minus(self, vec): 170 | return Vector2d(self.x - vec.x, self.y - vec.y) 171 | 172 | def crossProduct(self, vec): 173 | return (self.x * vec.y - self.y * vec.x) 174 | 175 | def isInTriangle(x1, y1, x2, y2, x3, y3, x, y): 176 | A = Vector2d(x1, y1) 177 | B = Vector2d(x2, y2) 178 | C = Vector2d(x3, y3) 179 | P = Vector2d(x, y) 180 | PA = A.minus(P) 181 | PB = B.minus(P) 182 | PC = C.minus(P) 183 | t1 = PA.crossProduct(PB) 184 | t2 = PB.crossProduct(PC) 185 | t3 = PC.crossProduct(PA) 186 | if (t1 * t2 >= 0) and (t1 * t3 >= 0): 187 | return True 188 | return False 189 | 190 | def getHexMapIndex(x, y): 191 | X_LEN = c.HEX_X_SIZE // 2 192 | Y_LEN = c.HEX_Y_SIZE // 2 193 | tmp_x, offset_x = divmod(x, c.HEX_X_SIZE) 194 | tmp_y, offset_y = divmod(y, Y_LEN * 3) 195 | map_x, map_y = 0, 0 196 | if offset_y <= (Y_LEN + Y_LEN//2): 197 | if offset_y >= Y_LEN//2: 198 | map_x, map_y = tmp_x, tmp_y * 2 199 | else: 200 | triangle_list = [(0, 0, 0, Y_LEN//2, X_LEN, 0), 201 | (0, Y_LEN//2, X_LEN, 0, c.HEX_X_SIZE, Y_LEN//2), 202 | (X_LEN, 0, c.HEX_X_SIZE, 0, c.HEX_X_SIZE, Y_LEN//2)] 203 | map_list = [(tmp_x - 1, tmp_y * 2 -1), (tmp_x, tmp_y * 2), (tmp_x, tmp_y * 2 -1)] 204 | for i, data in enumerate(triangle_list): 205 | if isInTriangle(*data, offset_x, offset_y): 206 | map_x, map_y = map_list[i] 207 | break 208 | elif offset_y >= c.HEX_Y_SIZE: 209 | if offset_x <= X_LEN: 210 | map_x, map_y = tmp_x - 1, tmp_y * 2 + 1 211 | else: 212 | map_x, map_y = tmp_x, tmp_y *2 + 1 213 | else: 214 | triangle_list = [(0, Y_LEN + Y_LEN//2, 0, c.HEX_Y_SIZE, X_LEN, c.HEX_Y_SIZE), 215 | (0, Y_LEN + Y_LEN//2, X_LEN, c.HEX_Y_SIZE, c.HEX_X_SIZE, Y_LEN + Y_LEN//2), 216 | (X_LEN, c.HEX_Y_SIZE, c.HEX_X_SIZE, Y_LEN + Y_LEN//2, c.HEX_X_SIZE, c.HEX_Y_SIZE)] 217 | map_list = [(tmp_x - 1, tmp_y * 2 + 1), (tmp_x, tmp_y * 2), (tmp_x, tmp_y *2 + 1)] 218 | for i, data in enumerate(triangle_list): 219 | if isInTriangle(*data, offset_x, offset_y): 220 | map_x, map_y = map_list[i] 221 | break 222 | if map_x == 0 and map_y == 0: 223 | print('pos[%d, %d](%d, %d) base[%d, %d] off[%d, %d] ' % (map_x, map_y, x, y, tmp_x, tmp_y, offset_x, offset_y)) 224 | return (map_x, map_y) 225 | 226 | pg.init() 227 | pg.display.set_caption(c.ORIGINAL_CAPTION) 228 | SCREEN = pg.display.set_mode(c.SCREEN_SIZE) 229 | 230 | GFX = load_all_gfx(os.path.join("resources","graphics")) 231 | ATTR = load_entiry_attr(os.path.join('source', 'data', 'entity.json')) 232 | GRID = load_map_grid_image() --------------------------------------------------------------------------------