Skip to content

Commit

Permalink
Better map event distribution (#113)
Browse files Browse the repository at this point in the history
* add probabilities to events

* Implement map generation rules

* add map generation rule tests

* add map regeneration functionality
  • Loading branch information
Lann-hyl authored Sep 9, 2024
1 parent b94b6a8 commit ef72535
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 44 deletions.
6 changes: 1 addition & 5 deletions #Scenes/Events/mob/0.tscn
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
[gd_scene load_steps=16 format=3 uid="uid://sg1wi7uqvv25"]
[gd_scene load_steps=15 format=3 uid="uid://sg1wi7uqvv25"]

[ext_resource type="Script" path="res://#Scenes/SceneScripts/TestingScene.gd" id="1_nmgwp"]
[ext_resource type="PackedScene" uid="uid://bcpmrmofcilbn" path="res://Core/Battler.tscn" id="2_e6pjn"]
[ext_resource type="Script" path="res://Managers/MapManager.gd" id="3_fnmf8"]
[ext_resource type="PackedScene" uid="uid://clmg3l3n28x38" path="res://Entity/Player/Player.tscn" id="4_ss8ob"]
[ext_resource type="Script" path="res://#Scenes/SceneScripts/TestingScene_UIcontrol.gd" id="5_8ejph"]
[ext_resource type="PackedScene" uid="uid://dpjfy4pv0vxst" path="res://Cards/CardContainer.tscn" id="6_dwemq"]
Expand All @@ -23,9 +22,6 @@ metadata/_edit_vertical_guides_ = [1216.0]
[node name="Battler" parent="." instance=ExtResource("2_e6pjn")]
position = Vector2(896, 364)

[node name="TestMap" type="Node2D" parent="."]
script = ExtResource("3_fnmf8")

[node name="Player" parent="." instance=ExtResource("4_ss8ob")]
position = Vector2(986, 631)

Expand Down
13 changes: 1 addition & 12 deletions Event/EventRandom.gd
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,7 @@ func get_room_abbreviation() -> String:
static func choose_other_event() -> EventBase:
# Choose an index
# This assumes the random event is the element 0 of the enum, meaning we don't select it
var enum_index: int = randi_range(1, GlobalEnums.EventType.size() - 1)
match enum_index:
GlobalEnums.EventType.Heal:
return EventHeal.new()
GlobalEnums.EventType.Mob:
return EventMob.new()
GlobalEnums.EventType.Shop:
return EventShop.new()
GlobalEnums.EventType.Dialogue:
return EventDialogue.new()
# this way even if the random is not in position 0 and we end up selecting it due to unluck, it will continue to try selecting another event
return EventRandom.new()
return GlobalEnums.choose_event_from_type(randi_range(1, GlobalEnums.EventType.size() - 1))


## @Override
Expand Down
3 changes: 3 additions & 0 deletions Global/DEBUG_VAR.gd
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ const DEBUG_SKIP_EVENT: bool = false
## in the inventory UI
const DEBUG_ACTIVE_INVENTORY_DEBUG_BUTTONS : bool = false

## Temporary debug option to print the number of each generated event type [br]
## TO BE REMOVED after implmentation
const DEBUG_PRINT_EVENT_COUNT : bool = false
11 changes: 9 additions & 2 deletions Global/GLOBAL_VAR.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ var MODIFIER_KEYS: Dictionary = {
# Keys can be accessed with the syntax: GlobalVar.MODIFIER_KEYS.ADD_PERMANENT
# this is useful if we need to change the reference value in multiple places

## A list of all the possible events
var EVENTS_CLASSIFICATION: Array[Resource] = [EventMob, EventRandom, EventShop, EventHeal, EventDialogue]

## A list of the probabilities of all possible events
var EVENTS_PROBABILITIES: Dictionary = {
GlobalEnums.EventType.Random : 16,
GlobalEnums.EventType.Heal : 12,
GlobalEnums.EventType.Mob : 45,
GlobalEnums.EventType.Shop : 5,
GlobalEnums.EventType.Dialogue: 22
}

## A list of all the possible movements on the map
var POSSIBLE_MOVEMENTS: Dictionary = {
Expand Down
18 changes: 17 additions & 1 deletion Global/Global_Enums.gd
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,27 @@ enum PossibleModifierNames {

## All the possible types of events [br]
## @experimental
##! [method EventRandom.choose_other_event] should be updated if you add a new event
##! [method choose_event_from_type] and [var Global_var.EVENTS_PROBABILITIES] should be updated if you add a new event
enum EventType {
Random, # ! Random should always be the first element (see EventRandom)
Heal,
Mob,
Shop,
Dialogue,
}

## Helper function that returns the Event resource depending on the given EventType
static func choose_event_from_type(event_type: EventType) -> EventBase:
match event_type:
GlobalEnums.EventType.Random:
return EventRandom.new()
GlobalEnums.EventType.Heal:
return EventHeal.new()
GlobalEnums.EventType.Mob:
return EventMob.new()
GlobalEnums.EventType.Shop:
return EventShop.new()
GlobalEnums.EventType.Dialogue:
return EventDialogue.new()
# A case for an EventType has not been defined, so we arbitrarily return Random
return EventRandom.new()
216 changes: 200 additions & 16 deletions Managers/MapManager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,33 @@ var current_map: MapBase
## - Width should be odd (for symmetry + padding reason) [br]
## - Width should not increase or decrease by more than 2 per floor (this makes it certain all rooms on the map are accessible [br]
var map_width_array: Array[int]


## Room events that we do not want to be consecutive
const no_consecutive_room_event: Array[String] = ["shop", "heal"]

## List containing all events that we will pick from to populate the map
var event_list: Array[GlobalEnums.EventType]
const event_count_deviation: float = 0.5

## Number of times a room type can be picked. If we cannot find a suitable room type in map_reset_limit tries, we regenerate all room events
const map_reset_limit: int = 10

#map_floors_width changes the width of the map's floors
## Generates and Populates a map with rooms that have random room types. More in depth algorithms will be added in the future
func create_map(map_floors_width: Array[int] = map_width_array) -> MapBase:
func create_map(map_floors_width: Array[int] = map_width_array) -> MapBase:
var _map: MapBase = MapBase.new()
# 2d array to return. this will be populated with rooms
var _grid: Array[Array] = []
var _grid: Array[Array] = []
var _max_floor_size: int = map_floors_width.max()
# loop through the floor, add padding, rooms and padding again
for index_height: int in range(map_floors_width.size()):
for index_height: int in range(map_floors_width.size()):

var _floor_width : int = map_floors_width[index_height]
var _floor_width: int = map_floors_width[index_height]
# ! only works if the size of the floor is odd

var _padding_size : int = floor((_max_floor_size - _floor_width)/2.)
var _padding_size: int = floor((_max_floor_size - _floor_width) / 2.)

var _padding:Array = []
var _padding: Array = []
_padding.resize(_padding_size)
_padding.fill(null)
_grid.append([])
Expand All @@ -51,29 +61,203 @@ func create_map(map_floors_width: Array[int] = map_width_array) -> MapBase:

# loop through positions of the grid and assign a room type
for index_width: int in range(map_floors_width[index_height]):
# randomly choose a room type
var _rand_type_index: int = randi_range(0, GlobalVar.EVENTS_CLASSIFICATION.size() - 1)
var _room_event: EventBase = GlobalVar.EVENTS_CLASSIFICATION[_rand_type_index].new()
# create a new room with the room type and give it its position
# create a new room with the null type and give it its position
var _generated_room: RoomBase = RoomBase.new()
_generated_room.room_event = _room_event
_generated_room.room_event = null
_generated_room.room_position = Vector2i(index_width + _padding_size, index_height)
# put the new room on the grid

_grid[index_height].append(_generated_room as RoomBase)
_grid[index_height].append_array(_padding)
_map.rooms = _grid

# Assign room type to all rooms
assign_events(_map)

return _map as MapBase

## Checks if the current room event upholds all map generation rules
func is_room_event_correct(current_room: RoomBase, map: MapBase = current_map) -> bool:
## Rule 0: A room cannot have no event
if current_room.room_event == null:
return false

var current_room_event_name: String = current_room.room_event.get_event_name()

## Rule 1: No Heal room before half of the map
if current_room_event_name == "heal" && current_room.room_position.y < int(floor(map.rooms.size() / 2.0)):
return false


## Rule 2: Cannot have 2 consecutive Heal or Shop rooms
# We need to check the parents (We define parents as rooms from the floor below that can reach the current room)
# if current room event is shop or heal
if current_room_event_name in no_consecutive_room_event:
# Get the parent rooms' types
var _parent_events: Array[String] = []
var _parent_y: int = current_room.room_position.y - 1

if _parent_y >= 0:
for delta_x: int in [-1, 0, 1]:
var _parent_x: int = current_room.room_position.x + delta_x
if _parent_x >= 0 && _parent_x < map.rooms[_parent_y].size() && map.rooms[_parent_y][_parent_x] != null:
_parent_events.append(map.rooms[_parent_y][_parent_x].room_event.get_event_name())

# Check if current room has the same type as one of its parent room
if current_room_event_name in _parent_events:
return false


## Rule 3: There must be at least 2 room types among destinations of Rooms that have 2 or more Paths going out.
## Since the events of the rooms on the right of current room ((x+i,y), i=1,2,...) have not been generated yet,
## we don't need to take them into account. We only check neighbors on the left: (x-2,y), (x-1,y) and (x,y)
## Note: We make sure the two leftmost and rightmost rooms of the floor have different events,
## since they could be the only destinations of a room from the previous floor (happens when current floor's size
## is smaller or equal than previous)
var _neighbors_events: Array[String] = []
var delta_xs: Array[int] = []

# If rightmost room of floor
if current_room.room_position.x == map.rooms[current_room.room_position.y].size() - 1 or map.rooms[current_room.room_position.y][current_room.room_position.x + 1] == null:
delta_xs = [-1, 0]
else:
delta_xs = [-2, -1, 0]

# Get the neighbor and current rooms types
var _neighbor_y: int = current_room.room_position.y
for delta_x: int in delta_xs:
var _neighbor_x: int = current_room.room_position.x + delta_x
if _neighbor_x >= 0 && _neighbor_x < map.rooms[_neighbor_y].size() && map.rooms[_neighbor_y][_neighbor_x] != null:
_neighbors_events.append(map.rooms[_neighbor_y][_neighbor_x].room_event.get_event_name())

# If our current room has no neighbors on the left, ignore this rule
if _neighbors_events.size() >= 2:
var _unique: Array[String] = []

# Remove duplicates
for event in _neighbors_events:
if not _unique.has(event):
_unique.append(event)

# If there is only one event type among all rooms, redraw event
if _unique.size() == 1:
return false


## Rule 4: No Heal rooms two floors before Boss
if (current_room_event_name == "heal" && current_room.room_position.y == map.rooms.size() - 3):
return false

return true

## Creates the list that contains all events that we will be picking from to populate the map
## The number of occurences of each event is defined by the total number of rooms times the probability of the event set in Global_var.gd
## We then add some extra events (deviation) to avoid being locked, there are rules that could prevent the remaining event types to be picked, resulting in a deadlock
## While the number of each event is not exactly the expected number, it should be close enough to what we want
func create_event_list() -> void:
event_list = []
var total_nb_rooms: int = 0
for nb: int in map_width_array:
total_nb_rooms += nb

for event_type: GlobalEnums.EventType in GlobalVar.EVENTS_PROBABILITIES:
var event_type_list: Array[GlobalEnums.EventType] = []
var event_count: int = floor(total_nb_rooms * GlobalVar.EVENTS_PROBABILITIES[event_type]/100)

event_type_list.resize(event_count + ceil(event_count*event_count_deviation))
event_type_list.fill(event_type)
event_list.append_array(event_type_list)

## Pick an event randomly from the list
func pick_room_type() -> GlobalEnums.EventType:
return event_list.pick_random()

## Assign events to existing rooms with set probabilities.
func assign_events(map: MapBase = current_map) -> void:
create_event_list()
var map_reset_counter: int = 0

# Scan the whole map, assign events to valid rooms
for index_height: int in range(map.rooms.size()):
for index_width: int in range(map.rooms[index_height].size()):
# If not a room, ignore
if map.rooms[index_height][index_width] == null:
continue

var _current_room: RoomBase = map.rooms[index_height][index_width]

# Set fixed rooms here
if index_height == map.rooms.size() - 2:
# Rooms before boss are Heal rooms
_current_room.room_event = EventHeal.new()

elif index_height == map.rooms.size() - 1:
# Boss room, TO BE ASSIGNED
_current_room.room_event = EventMob.new()

else:
var room_type: GlobalEnums.EventType
map_reset_counter = 0
while not is_room_event_correct(_current_room, map) and map_reset_counter < map_reset_limit:
map_reset_counter += 1
room_type = pick_room_type()
_current_room.room_event = GlobalEnums.choose_event_from_type(room_type)

# If we couldn't find a suitable room type in map_reset_limit tries, we regenerate the map
if map_reset_counter >= map_reset_limit:
for reset_index_height: int in range(map.rooms.size()):
for reset_index_width: int in range(map.rooms[reset_index_height].size()):
if map.rooms[reset_index_height][reset_index_width] == null:
continue

map.rooms[reset_index_height][reset_index_width].room_event = null
assign_events(map)
return

# If assigned, we remove the event from the list only after we are sure it does not conflict with any rules
event_list.erase(room_type)


## Create a map with a width array

func _ready() -> void:
map_width_array = [1, 3, 5, 7, 9, 11, 9, 7, 5, 3, 1]
current_map = create_map()
if not is_map_initialized():
map_width_array = [1, 3, 5, 7, 9, 11, 9, 7, 5, 3, 1]
current_map = create_map()


## checks if the map exists
##### DEBUG #####
# Count number of each event
if DebugVar.DEBUG_PRINT_EVENT_COUNT:
debug_print_event_count()


## checks if the map exists
func is_map_initialized() -> bool:
return current_map != null

## DEBUG
## Prints the number and percentage of each event type, and the corresponding expected number as well
func debug_print_event_count() -> void:
var events: Dictionary = {}
var total_nb_rooms: int = 0
var expected_probabilities: Dictionary = {}

# Loop over event probabilities to create a dict<event_name, event_probability>
# since we use the event names to identify the type of event,
# and we do not currently have a way to get the EventType from the Event Resource (and thus cannot get the probability from the resource)
for event_type: GlobalEnums.EventType in GlobalVar.EVENTS_PROBABILITIES:
expected_probabilities[GlobalEnums.choose_event_from_type(event_type).get_event_name()] = GlobalVar.EVENTS_PROBABILITIES[event_type]

for index_height: int in range(current_map.rooms.size()):
for index_width: int in range(current_map.rooms[index_height].size()):
if current_map.rooms[index_height][index_width] == null:
continue

var event: String = current_map.rooms[index_height][index_width].room_event.get_event_name()
if not event in events:
events[event] = 0
events[event] += 1
total_nb_rooms += 1
for k: String in events:
print("Event " + k + " has " + str(events[k]) + " rooms (expected: "+ str(float(expected_probabilities[k] * total_nb_rooms/100.0)).pad_decimals(2) +"). (The percentage is " + str(float(events[k]) * 100 / total_nb_rooms).pad_decimals(2) + "%, expected: " + str(expected_probabilities[k]) + "%)")
print("Total number of rooms generated: " + str(total_nb_rooms))
19 changes: 11 additions & 8 deletions Tests/test_eventbase.gd
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ func after_each() -> void:
assert_no_new_orphans("Orphans still exist, please free up test resources.")

func test_event_base_array_all_types() -> void:
var event0: Resource = GlobalVar.EVENTS_CLASSIFICATION[0]
assert_eq(event0, EventMob)
var event0: Resource = GlobalEnums.choose_event_from_type(GlobalEnums.EventType.Random)
assert_true(event0 is EventRandom)

var event1: Resource = GlobalVar.EVENTS_CLASSIFICATION[1]
assert_eq(event1, EventRandom)
var event1: Resource = GlobalEnums.choose_event_from_type(GlobalEnums.EventType.Heal)
assert_true(event1 is EventHeal)

var event2: Resource = GlobalVar.EVENTS_CLASSIFICATION[2]
assert_eq(event2, EventShop)
var event2: Resource = GlobalEnums.choose_event_from_type(GlobalEnums.EventType.Mob)
assert_true(event2 is EventMob)

var event3: Resource = GlobalVar.EVENTS_CLASSIFICATION[3]
assert_eq(event3, EventHeal)
var event3: Resource = GlobalEnums.choose_event_from_type(GlobalEnums.EventType.Shop)
assert_true(event3 is EventShop)

var event4: Resource = GlobalEnums.choose_event_from_type(GlobalEnums.EventType.Dialogue)
assert_true(event4 is EventDialogue)

Loading

0 comments on commit ef72535

Please sign in to comment.