Separating game/scene logic

This commit is contained in:
duckduckdoof 2025-02-05 15:33:07 -05:00
parent ebb1460e25
commit 455f392965
9 changed files with 334 additions and 225 deletions

39
global_config.py Normal file
View file

@ -0,0 +1,39 @@
"""
global_config.py
author: Caleb Scott
Configuration file for global constants which are relevant for both
game scene and game logic.
"""
# CONSTANTS ---------------------------------------------------------
# Size of game board
BOARD_WIDTH = 30
BOARD_HEIGHT = 20
# Object Types (applies to both tiles and game objects)
# Structure Layer
LOGGER = "Logger"
CROPS = "Crops"
HYDRO_POWER = "HydroPower"
HOUSING = "Housing"
MINE = "Mine"
FACTORY = "Factory"
JUNCTION = "Junction"
WOODSILO = "WoodSilo"
ROCKSILO = "RockSilo"
METALSILO = "MetalSilo"
CAPACITOR = "Capacitor"
WATER_TOWER = "WaterTower"
# Foundation Layer
FOUNDATION = "Foundation"
# Environment Layer
GROUND = "Ground"
WATER = "Water"
METAL = "Metal"
TREES = "Trees"
ROCKS = "Rocks"

61
lib/engine/game_board.py Normal file
View file

@ -0,0 +1,61 @@
"""
game_board.py
author: Caleb Scott
Representation of the game board/space. Ideally, this module can be
modified/swapped if the geometry of the game changes (2D-3D, etc.)
"""
# CLASSES -----------------------------------------------------------
class FlatWorld:
def __init__(self, init_space: list):
# We assume that the initial space is a rectangular 2D array
# Initialize the world space (2D array)
self.space = init_space
self.width = len(init_space[0])
self.height = len(init_space)
def get_at(self, x, y):
"""
Returns object located at (x,y) coordinate of space.
"""
if (x >= 0 and x < self.width) and (y >= 0 and y < self.height):
return self.space[x][y]
else:
return None
def set_at(self, x, y, object):
"""
Sets object located at (x,y) coordinate of space.
"""
if (x >= 0 and x < self.width) and (y >= 0 and y < self.height):
self.space[x][y] = object
return True
else:
return False
class LayeredFlatWorld:
def __init__(self, layers: dict):
# We assume that layers is a dictionary of 2D arrays.
# Initialize the layers of 2D space
self.layers = {}
for layer in layers.keys():
self.layers[layer] = FlatWorld(layers[layer])
def get_at(self, x, y, layer):
"""
Gets object at (x,y) coordinate at layer
"""
return self.layers[layer].get_at(x, y)
def set_at(self, x, y, layer, object):
"""
Sets object located at (x,y) coordinate at layer
"""
self.layers[layer].set_at(x, y, object)

View file

@ -7,4 +7,21 @@ Configuration file for game objects (like setting default rates, types, etc.).
This is not the same as scene_config.py, which configures the scene (visuals, sprites, etc.).
"""
# CONSTANTS ---------------------------------------------------------
# CONSTANTS ---------------------------------------------------------
# Game Starting Resources
STARTING_RESOURCES = {
"people": 10,
"food": 200,
"metal": 50,
"wood": 100,
"rock": 100
}
# Resource types
RESOURCE_MANPOWER = "Manpower"
RESOURCE_WATER = "Water"
RESOURCE_METALS = "Metals"
RESOURCE_ROCKS = "Rocks"
RESOURCE_WOOD = "Wood"
RESOURCE_POWER = "Power"

View file

@ -9,100 +9,108 @@ visual elements; this is handles in the scene/ python files
# IMPORTS -----------------------------------------------------------
import arcade
from lib.scene.scene_config import *
from lib.scene.scene_tiles import *
from lib.engine.game_config import *
from lib.engine.game_board import LayeredFlatWorld
# FUNCTIONS ---------------------------------------------------------
def init_gb_from_scene_info(scene_info: dict):
"""
Takes a dictionary of 2D arrays (organized by layer name), and
converts them into their proper game board of game objects.
"""
# CLASSES -----------------------------------------------------------
class GameLogic:
def __init__(self, scene, starting_resources):
def __init__(self, layers, starting_resources=STARTING_RESOURCES):
# Initializes the game board (from arcade Scene)
# and starting resources
self.scene = scene
self.game_board = LayeredFlatWorld(layers)
self.resources = starting_resources
def place_structure(self, struct_type, x, y):
"""
Game logic for placing a structure, if valid.
"""
# First check and see if we already have a structure there.
s_tiles = arcade.get_sprites_at_point((x,y), self.scene[LAYER_STRUCTURES])
if len(s_tiles) == 1:
return "Structure already in this space."
# def place_structure(self, struct_type, x, y):
# """
# Game logic for placing a structure, if valid.
# """
# # First check and see if we already have a structure there.
# s_tiles = arcade.get_sprites_at_point((x,y), self.scene[LAYER_STRUCTURES])
# if len(s_tiles) == 1:
# return "Structure already in this space."
# Get the centered location of the tile chosen
# We use the environment layer instead to determine if placement
# is legal.
e_tiles = arcade.get_sprites_at_point((x,y), self.scene[LAYER_ENVIRONMENT])
if len(e_tiles) == 1:
print(e_tiles[0].properties)
env_tile_type = e_tiles[0].properties['class']
c_x, c_y = e_tiles[0].center_x, e_tiles[0].center_y
# # Get the centered location of the tile chosen
# # We use the environment layer instead to determine if placement
# # is legal.
# e_tiles = arcade.get_sprites_at_point((x,y), self.scene[LAYER_ENVIRONMENT])
# if len(e_tiles) == 1:
# print(e_tiles[0].properties)
# env_tile_type = e_tiles[0].properties['class']
# c_x, c_y = e_tiles[0].center_x, e_tiles[0].center_y
# Determine what sprite object to place
if struct_type == LOGGER:
if env_tile_type == TREES:
logger_sprite = LoggerTile(c_x, c_y)
self.scene.add_sprite(LAYER_STRUCTURES, logger_sprite)
return ""
else:
return "Logger must be placed on trees tile."
elif struct_type == CROPS:
if env_tile_type == GROUND:
crops_sprite = CropsTile(c_x, c_y)
self.scene.add_sprite(LAYER_STRUCTURES, crops_sprite)
return ""
else:
return "Crops must be placed on a ground tile."
elif struct_type == HYDROPOWER:
if env_tile_type == WATER:
hydro_sprite = HyroPowerTile(c_x, c_y)
self.scene.add_sprite(LAYER_STRUCTURES, hydro_sprite)
return ""
else:
return "Hydro Power must be placed on a water tile."
elif struct_type == HOUSING:
if env_tile_type == GROUND:
housing_sprite = HousingTile(c_x, c_y)
self.scene.add_sprite(LAYER_STRUCTURES, housing_sprite)
return ""
else:
return "Housing must be placed on a ground tile."
elif struct_type == MINER:
if env_tile_type == IRON:
miner_sprite = MinerTile(c_x, c_y)
self.scene.add_sprite(LAYER_STRUCTURES, miner_sprite)
return ""
else:
return "Miner must be placed on an iron tile."
elif struct_type == FACTORY:
if env_tile_type == GROUND:
factory_sprite = FactoryTile(c_x, c_y)
self.scene.add_sprite(LAYER_STRUCTURES, factory_sprite)
return ""
else:
return "Factory must be placed on a ground tile."
elif struct_type == JUNCTION:
if env_tile_type == GROUND:
junction_sprite = JunctionTile(c_x, c_y)
self.scene.add_sprite(LAYER_STRUCTURES, junction_sprite)
return ""
else:
return "Junctions must be placed on a ground tile."
else:
return ""
# # Determine what sprite object to place
# if struct_type == LOGGER:
# if env_tile_type == TREES:
# logger_sprite = LoggerTile(c_x, c_y)
# self.scene.add_sprite(LAYER_STRUCTURES, logger_sprite)
# return ""
# else:
# return "Logger must be placed on trees tile."
# elif struct_type == CROPS:
# if env_tile_type == GROUND:
# crops_sprite = CropsTile(c_x, c_y)
# self.scene.add_sprite(LAYER_STRUCTURES, crops_sprite)
# return ""
# else:
# return "Crops must be placed on a ground tile."
# elif struct_type == HYDROPOWER:
# if env_tile_type == WATER:
# hydro_sprite = HyroPowerTile(c_x, c_y)
# self.scene.add_sprite(LAYER_STRUCTURES, hydro_sprite)
# return ""
# else:
# return "Hydro Power must be placed on a water tile."
# elif struct_type == HOUSING:
# if env_tile_type == GROUND:
# housing_sprite = HousingTile(c_x, c_y)
# self.scene.add_sprite(LAYER_STRUCTURES, housing_sprite)
# return ""
# else:
# return "Housing must be placed on a ground tile."
# elif struct_type == MINER:
# if env_tile_type == IRON:
# miner_sprite = MinerTile(c_x, c_y)
# self.scene.add_sprite(LAYER_STRUCTURES, miner_sprite)
# return ""
# else:
# return "Miner must be placed on an iron tile."
# elif struct_type == FACTORY:
# if env_tile_type == GROUND:
# factory_sprite = FactoryTile(c_x, c_y)
# self.scene.add_sprite(LAYER_STRUCTURES, factory_sprite)
# return ""
# else:
# return "Factory must be placed on a ground tile."
# elif struct_type == JUNCTION:
# if env_tile_type == GROUND:
# junction_sprite = JunctionTile(c_x, c_y)
# self.scene.add_sprite(LAYER_STRUCTURES, junction_sprite)
# return ""
# else:
# return "Junctions must be placed on a ground tile."
# else:
# return ""
def delete_structure(self, x, y):
"""
Game logic for deleting a structure, if valid.
"""
s_tiles = arcade.get_sprites_at_point((x,y), self.scene[LAYER_STRUCTURES])
if len(s_tiles) == 1:
struct_sprites = self.scene[LAYER_STRUCTURES]
struct_sprites.remove(s_tiles[0])
return ""
else:
return "No structure on that space."
# def delete_structure(self, x, y):
# """
# Game logic for deleting a structure, if valid.
# """
# s_tiles = arcade.get_sprites_at_point((x,y), self.scene[LAYER_STRUCTURES])
# if len(s_tiles) == 1:
# struct_sprites = self.scene[LAYER_STRUCTURES]
# struct_sprites.remove(s_tiles[0])
# return ""
# else:
# return "No structure on that space."

View file

@ -12,14 +12,50 @@ between the game scene/rendering and game logic/objects.
import math
# CONSTANTS ---------------------------------------------------------
from lib.engine.game_config import *
from global_config import *
MANPOWER = "Manpower"
WATER = "Water"
METALS = "Metals"
ROCKS = "Rocks"
WOOD = "Wood"
POWER = "Power"
# FUNCTIONS ---------------------------------------------------------
def str_to_game_object(game_obj_str: str):
"""
Factory method: given a game object string
(usually from tileset arrays), return the corresponding object.
"""
if game_obj_str == JUNCTION:
return Junction()
elif game_obj_str == HOUSING:
return Housing()
elif game_obj_str == ROCKSILO:
return Silo(RESOURCE_ROCKS)
elif game_obj_str == METALSILO:
return Silo(RESOURCE_METALS)
elif game_obj_str == WOODSILO:
return Silo(RESOURCE_WOOD)
elif game_obj_str == WATER_TOWER:
return WaterTower()
elif game_obj_str == CAPACITOR:
return Capacitor()
elif game_obj_str == HYDRO_POWER:
return HydroPower()
elif game_obj_str == CROPS:
return Crops()
elif game_obj_str == FACTORY:
return Factory()
elif game_obj_str == FOUNDATION:
return Foundation()
elif game_obj_str == WATER:
return Water()
elif game_obj_str == TREES:
return Trees()
elif game_obj_str == GROUND:
return Ground()
elif game_obj_str == METAL:
return Metals()
elif game_obj_str == ROCKS:
return Rocks()
else:
return None
# CLASSES -----------------------------------------------------------
@ -55,17 +91,17 @@ class Junction(GameObject):
class Housing(Storage):
def __init__(self):
super().__init__("Housing", MANPOWER, 20.0)
super().__init__("Housing", RESOURCE_MANPOWER, 20.0)
class WaterTower(Storage):
def __init__(self):
super().__init__("WaterTower", WATER, 600.0)
super().__init__("WaterTower", RESOURCE_WATER, 600.0)
class Capacitor(Storage):
def __init__(self):
super().__init__("Capacitor", POWER, 100.0)
super().__init__("Capacitor", RESOURCE_POWER, 100.0)
class Silo(Storage):
@ -94,6 +130,16 @@ class Quarry(Producer):
def __init__(self):
super().__init__("Quarry", 0.0)
class Crops(Producer):
def __init__(self):
super().__init__("Crops", 0.0)
class Factory(Producer):
def __init__(self):
super().__init__("Factory", 0.0)
## LAYER: Foundations
class Foundation(GameObject):
@ -105,7 +151,7 @@ class Foundation(GameObject):
class Water(Storage):
def __init__(self):
super().__init__("Water", WATER, math.inf, math.inf)
super().__init__("Water", RESOURCE_WATER, math.inf, math.inf)
class Ground(GameObject):
@ -115,14 +161,14 @@ class Ground(GameObject):
class Trees(Storage):
def __init__(self):
super().__init__("Trees", WOOD, 500.0, 500.0)
super().__init__("Trees", RESOURCE_WOOD, 500.0, 500.0)
class Metals(Storage):
def __init__(self):
super().__init__("Metals", METALS, 500.0, 500.0)
super().__init__("Metals", RESOURCE_METALS, 500.0, 500.0)
class Rocks(Storage):
def __init__(self):
super().__init__("Rocks", ROCKS, 500.0, 500.0)
super().__init__("Rocks", RESOURCE_ROCKS, 500.0, 500.0)

View file

@ -5,7 +5,7 @@ author: Caleb Scott
Configuration file for modifying the visual scene of the game.
This is not the same as engine/game_config.py, which is used to adjust
game objects (apart from their visuals, like sprites)
game objects (apart from their visuals)
"""
# CONSTANTS ---------------------------------------------------------
@ -18,7 +18,7 @@ SCREEN_WIDTH = 960
SCREEN_HEIGHT = 960
SCREEN_TITLE = "Miniopolis Demo"
# World size
# World size in pixels
WORLD_WIDTH = 960
WORLD_HEIGHT = 640
@ -41,30 +41,30 @@ JUNCTION_RES = RES + "connector.png"
MAPS = "maps/"
TEST_MAP = MAPS + "default-map.json"
# Tile property which gets its name as string
TILE_NAME = 'class'
# Layers for Map
LAYER_STRUCTURES = "Structures"
LAYER_FOUNDATIONS = "Foundations"
LAYER_ENVIRONMENT = "Environment"
# Tile Types
LOGGER = "LoggerTile"
CROPS = "CropsTile"
HYDROPOWER = "HydroPowerTile"
HOUSING = "HousingTile"
MINER = "MinerTile"
FACTORY = "FactoryTile"
JUNCTION = "JunctionTile"
# Layer ordering (bottom to top)
LAYERS = [
LAYER_ENVIRONMENT,
LAYER_FOUNDATIONS,
LAYER_STRUCTURES
]
FOUNDATION = "FoundationTile"
GROUND = "GroundTile"
WATER = "WaterTile"
METAL = "MetalTile"
TREES = "TreesTile"
# Game Starting Resources
STARTING_RESOURCES = {
"metal": 50,
"wood": 100,
"people": 10,
"food": 200
# Layer Options (for spatial hashing)
LAYER_OPTIONS = {
LAYER_ENVIRONMENT: {
"use_spatial_hash": True
},
LAYER_FOUNDATIONS: {
"use_spatial_hash": True
},
LAYER_STRUCTURES: {
"use_spatial_hash": True
}
}

36
lib/scene/scene_logic.py Normal file
View file

@ -0,0 +1,36 @@
"""
scene_logic.py
author: Caleb Scott
Logic for interpreting the visual elements of the game scene.
This includes importing tilesets and passing them to the game logic module,
interpreting player inputs on the screen, etc.
"""
# IMPORTS -----------------------------------------------------------
from arcade import Scene
from scene_config import *
from global_config import *
# FUNCTIONS ---------------------------------------------------------
def info_from_layered_tilemap(scene: Scene):
"""
Takes tilemap from arcade scene, and initializes arrays of
tile information from the tile objects.
This can then be passed to the game engine to convert these into
game objects
"""
scene_info = {}
for layer in LAYERS:
# Initialize empty game board (2D array)
scene_info[layer] = [[""] * BOARD_HEIGHT for _ in range(BOARD_WIDTH)]
# Populate the layer info as 2D arrays
for i, S in enumerate(scene[layer]):
scene_info[layer][i//BOARD_WIDTH][i%BOARD_HEIGHT] = scene[layer].properties[TILE_NAME]
return scene_info

View file

@ -1,91 +0,0 @@
"""
scene_tiles.py
author: Caleb Scott
Contains all classes of tiles used in miniopolis
"""
# IMPORTS -----------------------------------------------------------
import arcade
from lib.scene.scene_config import *
# CLASSES -----------------------------------------------------------
class HousingTile(arcade.Sprite):
def __init__(self, x, y):
self.image_file_name = HOUSING_RES
super().__init__(self.image_file_name, TILE_SCALE)
# Set custom properties
self.properties["type"] = HOUSING
self.center_x = x
self.center_y = y
class LoggerTile(arcade.Sprite):
def __init__(self, x, y):
self.image_file_name = LOGGER_RES
super().__init__(self.image_file_name, TILE_SCALE)
# Set custom properties
self.properties["type"] = LOGGER
self.center_x = x
self.center_y = y
class CropsTile(arcade.Sprite):
def __init__(self, x, y):
self.image_file_name = CROPS_RES
super().__init__(self.image_file_name, TILE_SCALE)
# Set custom properties
self.properties["type"] = CROPS
self.center_x = x
self.center_y = y
class HyroPowerTile(arcade.Sprite):
def __init__(self, x, y):
self.image_file_name = HYDRO_RES
super().__init__(self.image_file_name, TILE_SCALE)
# Set custom properties
self.properties["type"] = HYDROPOWER
self.center_x = x
self.center_y = y
class MinerTile(arcade.Sprite):
def __init__(self, x, y):
self.image_file_name = MINING_RES
super().__init__(self.image_file_name, TILE_SCALE)
# Set custom properties
self.properties["type"] = MINER
self.center_x = x
self.center_y = y
class FactoryTile(arcade.Sprite):
def __init__(self, x, y):
self.image_file_name = FACTORY_RES
super().__init__(self.image_file_name, TILE_SCALE)
# Set custom properties
self.properties["type"] = FACTORY
self.center_x = x
self.center_y = y
class JunctionTile(arcade.Sprite):
def __init__(self, x, y):
self.image_file_name = JUNCTION_RES
super().__init__(self.image_file_name, TILE_SCALE)
# Set custom properties
self.properties["type"] = JUNCTION
self.center_x = x
self.center_y = y

View file

@ -11,7 +11,6 @@ First attempt to make a tile-based colony-sim game.
import arcade
from lib.scene.scene_config import *
from lib.engine.game_logic import GameLogic
# CLASSES -----------------------------------------------------------
@ -28,22 +27,16 @@ class GameBoard(arcade.Window):
Used for starting up the game board
"""
# Obtain world tilemap
# Layer options come from https://api.arcade.academy/en/platformer_tutorial_revamp/tutorials/platform_tutorial/step_12.html
layer_options = {
LAYER_ENVIRONMENT: {
"use_spatial_hash": True
},
LAYER_STRUCTURES: {
"use_spatial_hash": True
}
}
self.tile_map = arcade.load_tilemap(
TEST_MAP,
scaling=TILE_SCALE,
layer_options=layer_options
layer_options=LAYER_OPTIONS
)
self.scene = arcade.Scene.from_tilemap(self.tile_map)
for layer in self.scene.keys():
print(layer)
# Tiles selection
self.selected_struct_tile = "[Nothing]"
self.selected_env_tile = GROUND
@ -84,7 +77,7 @@ class GameBoard(arcade.Window):
)
# Initialize the Game Logic class
self.game_logic = GameLogic(self.scene, STARTING_RESOURCES)
# self.game_logic = GameLogic(self.scene, None)
def on_key_release(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE: