From 273e3aba317741bb0bc48c0700ba2a4e81e39844 Mon Sep 17 00:00:00 2001 From: Russell Matney Date: Wed, 3 Apr 2024 17:11:11 -0400 Subject: [PATCH] feat: support constraints as arrays/dicts and with opts This code is crazy, hopefully the unit tests are enough to combat it. --- addons/core/Util.gd | 5 +- src/dino/modes/vania/RoomInputs.gd | 195 +++++++++++----------- src/dino/modes/vania/VaniaRoomDef.gd | 7 +- test/dino/modes/vania/room_inputs_test.gd | 53 +++++- 4 files changed, 151 insertions(+), 109 deletions(-) diff --git a/addons/core/Util.gd b/addons/core/Util.gd index 1c1a27255..c1a08526a 100644 --- a/addons/core/Util.gd +++ b/addons/core/Util.gd @@ -275,7 +275,10 @@ static func setup_popup_items(popup: PopupMenu, items: Array, on_select: Callabl ############################################################ -## random ########################################################### +## repeate/random ########################################################### + +static func repeat(s, n): + return range(n).map(func(_x): return s) static func rand_of(arr, n=1): if len(arr) == 0: diff --git a/src/dino/modes/vania/RoomInputs.gd b/src/dino/modes/vania/RoomInputs.gd index a1ca0476c..f27fbbdeb 100644 --- a/src/dino/modes/vania/RoomInputs.gd +++ b/src/dino/modes/vania/RoomInputs.gd @@ -65,14 +65,20 @@ var entities var room_shape var room_shapes var tilemap_scenes -var constraints func _init(opts={}): entities = opts.get("entities", []) room_shape = opts.get("room_shape") room_shapes = opts.get("room_shapes", []) tilemap_scenes = opts.get("tilemap_scenes", []) - constraints = opts.get("constraints", []) + +func to_printable(): + return { + entities=entities, + tilemap_scenes=tilemap_scenes, + room_shape=room_shape, + room_shapes=room_shapes, + } ## merge ###################################################### @@ -82,14 +88,11 @@ func merge(b: RoomInputs): room_shape=U._or(b.room_shape, room_shape), room_shapes=U.distinct(U.append_array(room_shapes, b.room_shapes)), tilemap_scenes=U.distinct(U.append_array(tilemap_scenes, b.tilemap_scenes)), - constraints=U.append_array(constraints, b.constraints), }) ## update room def ###################################################### func update_def(def: VaniaRoomDef): - def.constraints = constraints - if room_shape: def.local_cells = room_shape elif not room_shapes.is_empty(): @@ -127,39 +130,79 @@ static func merge_many(inputs): return inputs.reduce(func(a, b): return a.merge(b)) static func apply_constraints(conses, def: VaniaRoomDef): - var shape = def.local_cells - var ri = conses.reduce(RoomInputs.apply_constraint, RoomInputs.new()) + def.constraints = conses + + var existing_shape = def.local_cells + + if conses is RoomInputs: + conses.update_def(def) + if existing_shape != null and not existing_shape.is_empty(): + # maintain current local_cells + def.local_cells = existing_shape + return conses + + if conses is Dictionary: + conses = [conses] + + if not conses is Array: + Log.warn("Unhandled constraint collection passed to apply_constraints, aborting", conses) + return + + var ri = conses.map(func(cons): + # map constants to dicts with default opts + if cons is String: + return {cons: {}} + elif cons is Dictionary: + return cons + else: + Log.warn("Unhandled constraint passed to apply_constraints, ignoring", cons) + return null + ).reduce( + func(agg_inp, cons_dict): + if cons_dict == null: + return agg_inp + # apply each constraint_dict in succession + return RoomInputs.apply_constraint(agg_inp, cons_dict), + RoomInputs.new()) + ri.update_def(def) - if shape != null and not shape.is_empty(): - # maintain current local_cells - def.local_cells = shape + + if existing_shape != null and not existing_shape.is_empty(): + # maintain pre-existing local_cells + def.local_cells = existing_shape return ri -static func apply_constraint(inp: RoomInputs, constraint): - var cons_inp = RoomInputs.get_constraint_data(constraint) - return inp.merge(cons_inp) - -static func get_constraint_data(constraint): - match constraint: - IN_LARGE_ROOM: return large_room() - IN_SMALL_ROOM: return small_room() - IN_WOODEN_BOXES: return wooden_boxes() - IN_SPACESHIP: return spaceship() - IN_KINGDOM: return kingdom() - IN_VOLCANO: return volcano() - IN_GRASSY_CAVE: return grassy_cave() - - IS_COOKING_ROOM: return cooking_room() - - HAS_BOSS: return has_boss() - HAS_ENEMY: return has_enemy() - HAS_TARGET: return has_target() - HAS_LEAF: return has_leaf() - HAS_PLAYER: return has_player() - - HAS_COOKING_POT: return has_entity("CookingPot") - HAS_BLOB: return has_entity("Blob") - HAS_VOID: return has_entity("Void") +static func apply_constraint(inp: RoomInputs, cons_dict): + for k in cons_dict.keys(): + var cons_inp = RoomInputs.get_constraint_data(k, cons_dict.get(k)) + inp = inp.merge(cons_inp) + return inp + +static func get_constraint_data(cons_key, opts={}): + match cons_key: + IN_LARGE_ROOM: return large_room(opts) + IN_SMALL_ROOM: return small_room(opts) + IN_TALL_ROOM: return tall_room(opts) + IN_WIDE_ROOM: return wide_room(opts) + IN_WOODEN_BOXES: return wooden_boxes(opts) + IN_SPACESHIP: return spaceship(opts) + IN_KINGDOM: return kingdom(opts) + IN_VOLCANO: return volcano(opts) + IN_GRASSY_CAVE: return grassy_cave(opts) + + IS_COOKING_ROOM: return cooking_room(opts) + + HAS_BOSS: return has_boss(opts) + + HAS_ENEMY: return has_entity("Enemy", opts) + HAS_TARGET: return has_entity("Target", opts) + HAS_LEAF: return has_entity("Leaf", opts) + HAS_CANDLE: return has_entity("Candle", opts) + HAS_PLAYER: return has_entity("Player", opts) + HAS_COOKING_POT: return has_entity("CookingPot", opts) + HAS_BLOB: return has_entity("Blob", opts) + HAS_VOID: return has_entity("Void", opts) + _: return RoomInputs.new() @@ -192,32 +235,28 @@ const IN_SMALL_ROOM = "in_small_room" const IN_TALL_ROOM = "in_tall_room" const IN_WIDE_ROOM = "in_wide_room" -static func large_room(): +static func large_room(_opts={}): return RoomInputs.new({ room_shape=[all_room_shapes._4x, all_room_shapes._4x_wide].pick_random(), - constraints=[IN_LARGE_ROOM] }) -static func small_room(): +static func small_room(_opts={}): return RoomInputs.new({ room_shape=all_room_shapes.small, - constraints=[IN_SMALL_ROOM] }) -static func tall_room(): +static func tall_room(_opts={}): return RoomInputs.new({ room_shape=[all_room_shapes.tall, all_room_shapes.tall_3].pick_random(), - constraints=[IN_TALL_ROOM] }) -static func wide_room(): +static func wide_room(_opts={}): return RoomInputs.new({ room_shape=[ all_room_shapes.wide, all_room_shapes.wide_3, all_room_shapes.wide_4, ].pick_random(), - constraints=[IN_WIDE_ROOM] }) ## tilemaps ######################################################33 @@ -228,34 +267,29 @@ const IN_KINGDOM = "in_kingdom" const IN_VOLCANO = "in_volcano" const IN_GRASSY_CAVE = "in_grassy_cave" -static func wooden_boxes(): +static func wooden_boxes(_opts={}): return RoomInputs.new({ tilemap_scenes=["res://addons/reptile/tilemaps/WoodenBoxesTiles8.tscn",], - constraints=[IN_WOODEN_BOXES] }) -static func spaceship(): +static func spaceship(_opts={}): return RoomInputs.new({ tilemap_scenes=["res://addons/reptile/tilemaps/SpaceshipTiles8.tscn",], - constraints=[IN_SPACESHIP] }) -static func kingdom(): +static func kingdom(_opts={}): return RoomInputs.new({ tilemap_scenes=["res://addons/reptile/tilemaps/GildedKingdomTiles8.tscn",], - constraints=[IN_KINGDOM] }) -static func volcano(): +static func volcano(_opts={}): return RoomInputs.new({ tilemap_scenes=["res://addons/reptile/tilemaps/VolcanoTiles8.tscn",], - constraints=[IN_VOLCANO] }) -static func grassy_cave(): +static func grassy_cave(_opts={}): return RoomInputs.new({ tilemap_scenes=["res://addons/reptile/tilemaps/GrassyCaveTileMap8.tscn",], - constraints=[IN_GRASSY_CAVE] }) ## entities ######################################################33 @@ -270,64 +304,23 @@ const HAS_VOID = "has_void" const HAS_PLAYER = "has_player" const HAS_CANDLE = "has_candle" -static func has_entity(ent): - var cons - match ent: - "CookingPot": cons = HAS_COOKING_POT - "Blob": cons = HAS_BLOB - "Void": cons = HAS_VOID - var ri = RoomInputs.new({entities=[ent]}) - if cons: - ri.constraints = [cons] - return ri +static func has_entity(ent, opts={}): + var inp = RoomInputs.new({entities=U.repeat(ent, opts.get("count", 1))}) + return inp -static func has_boss(): +static func has_boss(_opts={}): return RoomInputs.new({ entities=[ ["Monstroar"], ["Beefstronaut"], ].pick_random(), - constraints=[HAS_BOSS], - }) - -static func has_enemy(): - return RoomInputs.new({ - entities=["Enemy"], - constraints=[HAS_ENEMY], - }) - -static func has_target(): - return RoomInputs.new({ - entities=["Target"], - constraints=[HAS_TARGET], - }) - -static func has_leaf(): - return RoomInputs.new({ - entities=["Leaf"], - constraints=[HAS_LEAF], - }) - -static func has_player(): - return RoomInputs.new({ - entities=["Player"], - constraints=[HAS_PLAYER], - }) - -static func has_candle(): - return RoomInputs.new({ - entities=["Candle"], - constraints=[HAS_CANDLE], }) ## encounters ######################################################33 const IS_COOKING_ROOM = "is_cooking_room" -static func cooking_room(): +static func cooking_room(_opts={}): return RoomInputs.new({ - entities=[ - ["Blob", "CookingPot", "Void"], - ].pick_random(), - constraints=[IS_COOKING_ROOM], - }).merge(large_room()) + entities=["Blob", "CookingPot", "Void"], + }) diff --git a/src/dino/modes/vania/VaniaRoomDef.gd b/src/dino/modes/vania/VaniaRoomDef.gd index 3264e923f..3c4cd1951 100644 --- a/src/dino/modes/vania/VaniaRoomDef.gd +++ b/src/dino/modes/vania/VaniaRoomDef.gd @@ -229,12 +229,7 @@ static func generate_defs(opts={}): entity_defs=e_defs, tile_defs=t_defs, tile_size=opts.get("tile_size") }) - if inputs is RoomInputs: - inputs.update_def(def) - elif inputs is Array: - RoomInputs.apply_constraints(inputs, def) - elif inputs is Dictionary: - RoomInputs.apply_constraints(inputs, def) + RoomInputs.apply_constraints(inputs, def) defs.append(def) return defs diff --git a/test/dino/modes/vania/room_inputs_test.gd b/test/dino/modes/vania/room_inputs_test.gd index 19892acbc..cebb00553 100644 --- a/test/dino/modes/vania/room_inputs_test.gd +++ b/test/dino/modes/vania/room_inputs_test.gd @@ -175,7 +175,7 @@ func test_room_def_inputs_player_room_sets_entities_and_shape(): assert_array(def.tilemap_scenes).contains(inp.tilemap_scenes) assert_array(def.tilemap_scenes).is_not_empty() - assert_array(inp.room_shape).is_not_null() + assert_that(inp.room_shape).is_not_null() assert_array(def.local_cells).is_not_empty() assert_array(def.local_cells).is_equal(inp.room_shape) @@ -218,3 +218,54 @@ func test_apply_constraints_keeps_local_cells(): _inp = RoomInputs.apply_constraints([RoomInputs.IN_SMALL_ROOM,], def) assert_array(def.local_cells).is_equal(my_local_cells) + +func test_apply_constraints_appends_multiple_entities(): + var def = VaniaRoomDef.new() + var inp = RoomInputs.apply_constraints([ + RoomInputs.HAS_TARGET, + RoomInputs.HAS_TARGET, + RoomInputs.HAS_PLAYER, + ], def) + + assert_array(inp.entities).contains(["Target", "Player"]) + assert_int(len(inp.entities)).is_equal(3) + assert_array(def.entities).contains(["Target", "Player"]) + assert_int(len(def.entities)).is_equal(3) + +func test_apply_constraints_supports_dicts_and_opts(): + var def = VaniaRoomDef.new() + # pass just a dict + var inp = RoomInputs.apply_constraints({ + RoomInputs.HAS_TARGET: {count=4}, + RoomInputs.HAS_PLAYER: {} + }, def) + + assert_array(inp.entities).contains(["Target", "Player"]) + assert_int(len(inp.entities)).is_equal(5) + assert_array(def.entities).contains(["Target", "Player"]) + assert_int(len(def.entities)).is_equal(5) + + def = VaniaRoomDef.new() + # pass just an array of dict + inp = RoomInputs.apply_constraints([{ + RoomInputs.HAS_TARGET: {count=4}, + RoomInputs.HAS_PLAYER: {} + }], def) + + assert_array(inp.entities).contains(["Target", "Player"]) + assert_int(len(inp.entities)).is_equal(5) + assert_array(def.entities).contains(["Target", "Player"]) + assert_int(len(def.entities)).is_equal(5) + + def = VaniaRoomDef.new() + # pass just an array of dicts + inp = RoomInputs.apply_constraints([{ + RoomInputs.HAS_TARGET: {count=4}, + }, { + RoomInputs.HAS_PLAYER: {} + }], def) + + assert_array(inp.entities).contains(["Target", "Player"]) + assert_int(len(inp.entities)).is_equal(5) + assert_array(def.entities).contains(["Target", "Player"]) + assert_int(len(def.entities)).is_equal(5)