Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Commit

Permalink
Merge pull request #172 from mxashlynn/rewrite-biome-detection
Browse files Browse the repository at this point in the history
All tests pass, runner and roller work as expected.
  • Loading branch information
mxashlynn committed May 5, 2020
2 parents 21b8580 + cdd0517 commit 7b1e886
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 237 deletions.
4 changes: 2 additions & 2 deletions Designer/BiomeConfiguration.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
LandThresholdFactor,LiquidThresholdFactor
1.25,0.25
LandThresholdFactor,LiquidThresholdFactor,RoomThresholdFactor
1.25,0.25,0.67
32 changes: 16 additions & 16 deletions Designer/BiomeModels.csv
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
ID,Name,Description,Comment,Tier,ElevationCategory,IsLiquidBased,ParquetCriteria,EntryRequirements
-30000,Biome Test,Test,Test,1,LevelGround,True,,
30000,Alpine,,,4,AboveGround,False,,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟SeasideKey∟TundraKey∟AlpineKey
30001,Cavern,,,3,BelowGround,False,,ForestKey∟CavernKey∟Tier2Key∟Tier3Key
30002,Desert,,,2,LevelGround,False,Deserted,SeasideKey∟DesertKey∟Tier2Key
30003,Field,,,0,LevelGround,False,,
30004,Forest,,,1,LevelGround,False,Forested,ForestKey
30005,Heavens,,,5,AboveGround,True,Heavenly,HeavenlyKey
30006,Inferno,,,5,BelowGround,True,Volcanic,InfernalKey
30007,Ruins,,,4,LevelGround,False,Ruinous,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟ForestKey∟SwampKey∟RuinsKey
30008,Seaside,,,1,LevelGround,True,Coastal,SeasideKey
30009,Swamp,,,2,LevelGround,False,Swampy,ForestKey∟SwampKey∟Tier2Key
30010,Town,,,0,LevelGround,False,HasBuildings,
30011,Tundra,,,2,LevelGround,False,Frozen,SeasideKey∟TundraKey∟Tier2Key
30012,Volcano,,,4,AboveGround,True,Volcanic,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟SeasideKey∟DesertKey∟VolcanoKey
30013,Prism,,,4,AboveGround,False,,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟SeasideKey∟TundraKey∟AlpineKey
ID,Name,Description,Comment,Tier,ElevationCategory,IsRoomBased,IsLiquidBased,ParquetCriteria,EntryRequirements
-30000,Biome Test,Test,Test,1,LevelGround,False,True,,
30000,Alpine,,,4,AboveGround,False,False,,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟SeasideKey∟TundraKey∟AlpineKey
30001,Cavern,,,3,BelowGround,False,False,,ForestKey∟CavernKey∟Tier2Key∟Tier3Key
30002,Desert,,,2,LevelGround,False,False,Deserted,SeasideKey∟DesertKey∟Tier2Key
30003,Field,,,0,LevelGround,False,False,,
30004,Forest,,,1,LevelGround,False,False,Forested,ForestKey
30005,Heavens,,,5,AboveGround,False,True,Heavenly,HeavenlyKey
30006,Inferno,,,5,BelowGround,False,True,Volcanic,InfernalKey
30007,Ruins,,,4,LevelGround,False,False,Ruinous,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟ForestKey∟SwampKey∟RuinsKey
30008,Seaside,,,1,LevelGround,False,True,Coastal,SeasideKey
30009,Swamp,,,2,LevelGround,False,False,Swampy,ForestKey∟SwampKey∟Tier2Key
30010,Town,,,0,LevelGround,True,False,,
30011,Tundra,,,2,LevelGround,False,False,Frozen,SeasideKey∟TundraKey∟Tier2Key
30012,Volcano,,,4,AboveGround,False,True,Volcanic,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟SeasideKey∟DesertKey∟VolcanoKey
30013,Prism,,,4,AboveGround,False,False,,ForestKey∟CavernKey∟Tier2Key∟Tier3Key∟SeasideKey∟TundraKey∟AlpineKey
96 changes: 73 additions & 23 deletions Documentation/Parquet_API.md

Large diffs are not rendered by default.

41 changes: 31 additions & 10 deletions ParquetClassLibrary/Biomes/BiomeConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,31 @@ public static class BiomeConfiguration
/// There must be at least this percentage of non-liquid <see cref="Parquets.ParquetModel"/>s in a given
/// <see cref="MapRegion"/> to generate the <see cref="BiomeModel"/> associated with them.
/// </summary>
internal static float LandThresholdFactor { get; private set; } = 1.25f;
internal static double LandThresholdFactor { get; private set; } = 1.25;

/// <summary>1 and 1/4th of a layers' worth of parquets must contribute to a land-based <see cref="BiomeModel"/>.</summary>
internal static float LandThreshold => ParquetsPerLayer * LandThresholdFactor;
/// <summary>How many of a layers' worth of parquets must contribute to a land-based <see cref="BiomeModel"/>.</summary>
internal static int LandThreshold
=> (int)Math.Round(ParquetsPerLayer * LandThresholdFactor, 0, MidpointRounding.AwayFromZero);

/// <summary>
/// There must be at least this percentage of liquid <see cref="Parquets.ParquetModel"/>s in a given
/// <see cref="MapRegion"/> to generate the <see cref="BiomeModel"/> associated with them.
/// </summary>
internal static float LiquidThresholdFactor { get; private set; } = 0.25f;
internal static double LiquidThresholdFactor { get; private set; } = 0.25;

/// <summary>3/4ths of a layers' worth of parquets must contribute to a fluid-based <see cref="BiomeModel"/>.</summary>
internal static float FluidThreshold => ParquetsPerLayer * LiquidThresholdFactor;
/// <summary>How many of a layers' worth of parquets must contribute to a Liquid-based <see cref="BiomeModel"/>.</summary>
internal static int LiquidThreshold
=> (int)Math.Round(ParquetsPerLayer * LiquidThresholdFactor, 0, MidpointRounding.AwayFromZero);

/// <summary>
/// There must be at least this percentage of liquid <see cref="Parquets.ParquetModel"/>s in a given
/// <see cref="MapRegion"/> to generate the <see cref="BiomeModel"/> associated with them.
/// </summary>
internal static double RoomThresholdFactor { get; private set; } = 0.67;

/// <summary>How many of a layers' worth of parquets must contribute to a room-based <see cref="BiomeModel"/>.</summary>
internal static int RoomThreshold
=> (int)Math.Round(ParquetsPerLayer * RoomThresholdFactor, 0, MidpointRounding.AwayFromZero);

#region Self Serialization
/// <summary>
Expand All @@ -51,7 +63,7 @@ public static void GetRecord()
var values = valueLine.Split(Delimiters.PrimaryDelimiter);

// Parse.
if (float.TryParse(values[0], out var temp))
if (double.TryParse(values[0], out var temp))
{
LandThresholdFactor = temp;
}
Expand All @@ -60,7 +72,7 @@ public static void GetRecord()
throw new FormatException(string.Format(CultureInfo.CurrentCulture, Resources.ErrorCannotParse,
values[0], nameof(LandThresholdFactor)));
}
if (float.TryParse(values[1], out temp))
if (double.TryParse(values[1], out temp))
{
LiquidThresholdFactor = temp;
}
Expand All @@ -69,6 +81,15 @@ public static void GetRecord()
throw new FormatException(string.Format(CultureInfo.CurrentCulture, Resources.ErrorCannotParse,
values[1], nameof(LiquidThresholdFactor)));
}
if (double.TryParse(values[2], out temp))
{
RoomThresholdFactor = temp;
}
else
{
throw new FormatException(string.Format(CultureInfo.CurrentCulture, Resources.ErrorCannotParse,
values[2], nameof(RoomThresholdFactor)));
}
}

/// <summary>
Expand All @@ -77,8 +98,8 @@ public static void GetRecord()
public static void PutRecord()
{
using var writer = new StreamWriter(GetFilePath(), false, new UTF8Encoding(true, true));
writer.Write($"{nameof(LandThresholdFactor)}{Delimiters.PrimaryDelimiter}{nameof(LiquidThresholdFactor)}\n");
writer.Write($"{LandThresholdFactor}{Delimiters.PrimaryDelimiter}{LiquidThresholdFactor}\n");
writer.Write($"{nameof(LandThresholdFactor)}{Delimiters.PrimaryDelimiter}{nameof(LiquidThresholdFactor)}{Delimiters.PrimaryDelimiter}{nameof(RoomThresholdFactor)}\n");
writer.Write($"{LandThresholdFactor}{Delimiters.PrimaryDelimiter}{LiquidThresholdFactor}{Delimiters.PrimaryDelimiter}{RoomThresholdFactor}\n");
}

/// <summary>
Expand Down
21 changes: 17 additions & 4 deletions ParquetClassLibrary/Biomes/BiomeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ namespace ParquetClassLibrary.Biomes
/// </summary>
public sealed class BiomeModel : Model
{
#region Class Defaults
/// <summary>Represents the lack of a <see cref="BiomeModel"/> for <see cref="MapRegion"/>s that fail to qualify.</summary>
public static BiomeModel None { get; } = new BiomeModel(ModelID.None, "Expanse", "A featureless region.", "The default biome.",
0, Elevation.LevelGround, false, false, null, null);
#endregion

#region Characteristics
/// <summary>
/// A rating indicating where in the progression this <see cref="BiomeModel"/> falls.
Expand All @@ -23,16 +29,20 @@ public sealed class BiomeModel : Model
[Index(5)]
public Elevation ElevationCategory { get; }

/// <summary>Determines whether or not this <see cref="BiomeModel"/> is defined in terms of liquid parquets.</summary>
/// <summary>Determines whether or not this <see cref="BiomeModel"/> is defined in terms of <see cref="Rooms.Room"/>s.</summary>
[Index(6)]
public bool IsRoomBased { get; }

/// <summary>Determines whether or not this <see cref="BiomeModel"/> is defined in terms of liquid parquets.</summary>
[Index(7)]
public bool IsLiquidBased { get; }

/// <summary>Describes the parquets that make up this <see cref="BiomeModel"/>.</summary>
[Index(7)]
[Index(8)]
public IReadOnlyList<ModelTag> ParquetCriteria { get; }

/// <summary>Describes the <see cref="ItemModel"/>s a <see cref="Beings.CharacterModel"/> needs to safely access this <see cref="BiomeModel"/>.</summary>
[Index(8)]
[Index(9)]
public IReadOnlyList<ModelTag> EntryRequirements { get; }
#endregion

Expand All @@ -46,19 +56,22 @@ public sealed class BiomeModel : Model
/// <param name="inComment">Comment of, on, or by the <see cref="BiomeModel"/>.</param>
/// <param name="inTier">A rating indicating where in the progression this <see cref="BiomeModel"/> falls.</param>
/// <param name="inElevationCategory">Describes where this <see cref="BiomeModel"/> falls in terms of the game world's overall topography.</param>
/// <param name="inIsRoomBased">Determines whether or not this <see cref="BiomeModel"/> is defined in terms of <see cref="Rooms.Room"/>s.</param>
/// <param name="inIsLiquidBased">Determines whether or not this <see cref="BiomeModel"/> is defined in terms of liquid parquets.</param>
/// <param name="inParquetCriteria">Describes the parquets that make up this <see cref="BiomeModel"/>.</param>
/// <param name="inEntryRequirements">Describes the <see cref="ItemModel"/>s needed to access this <see cref="BiomeModel"/>.</param>
public BiomeModel(ModelID inID, string inName, string inDescription, string inComment,
int inTier, Elevation inElevationCategory,
bool inIsLiquidBased, IEnumerable<ModelTag> inParquetCriteria,
bool inIsRoomBased, bool inIsLiquidBased,
IEnumerable<ModelTag> inParquetCriteria,
IEnumerable<ModelTag> inEntryRequirements)
: base(All.BiomeIDs, inID, inName, inDescription, inComment)
{
Precondition.MustBeNonNegative(inTier, nameof(inTier));

Tier = inTier;
ElevationCategory = inElevationCategory;
IsRoomBased = inIsRoomBased;
IsLiquidBased = inIsLiquidBased;
ParquetCriteria = (inParquetCriteria ?? Enumerable.Empty<ModelTag>()).ToList();
EntryRequirements = (inEntryRequirements ?? Enumerable.Empty<ModelTag>()).ToList();
Expand Down
137 changes: 136 additions & 1 deletion ParquetClassLibrary/Maps/MapRegion.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CsvHelper.Configuration.Attributes;
using ParquetClassLibrary.Biomes;
using ParquetClassLibrary.Parquets;
using ParquetClassLibrary.Rooms;

namespace ParquetClassLibrary.Maps
{
Expand Down Expand Up @@ -83,10 +86,16 @@ string IMapRegionEdit.Name

/// <summary>
/// Parquets that make up the region. If changing or replacing one of these,
/// remember to update the corresponding element in <see cref="MapRegion.ParquetStatuses"/>!
/// remember to update the corresponding element in <see cref="ParquetStatuses"/>!
/// </summary>
[Index(10)]
public override ParquetStackGrid ParquetDefinitions { get; }

/// <summary>
/// All of the <see cref="Rooms.Room"/>s detected in the <see cref="MapRegion"/>.
/// </summary>
[Ignore]
public RoomCollection Rooms { get; private set; }
#endregion
#endregion

Expand Down Expand Up @@ -120,6 +129,132 @@ string IMapRegionEdit.Name
}
#endregion

#region Analysis
/// <summary>
/// Determines which <see cref="BiomeModel"/> the given <see cref="MapRegion"/> corresponds to.
/// </summary>
/// <remarks>
/// This method assumes that <see cref="MapRegion.Rooms"/> has already been populated.
/// </remarks>
/// <returns>The appropriate <see cref="ModelID"/>.</returns>
public ModelID GetBiome()
{
foreach (BiomeModel biome in All.Biomes)
{
if (biome.ElevationCategory == ElevationLocal)
{
return FindBiomeByTag(this, biome);
}
}

// TODO Log a warning here.
// This is a degenerate case, as all three Elevations ought to have BiomeModels defined for them.
return BiomeModel.None.ID;

#region Local Helper Methods
// Determines if the given BiomeModel matches the given Region.
// inRegion -> The MapRegion to test.
// inBiome -> The BiomeModel to test against.
// Returns the given BiomeModel's ModelID if they match, otherwise returns the ModelID for the default biome.
static ModelID FindBiomeByTag(MapRegion inRegion, BiomeModel inBiome)
{
foreach (ModelTag biomeTag in inBiome.ParquetCriteria)
{
// Prioritization of biome categories is hard-coded in the following way:
// 1 Room-based Biomes supercede
// 2 Liquid-based Biomes supercede
// 3 Land-based Biomes supercede
// 4 the default Biome.
if ((inBiome.IsRoomBased
&& GetParquetsInRooms(inRegion) <= BiomeConfiguration.RoomThreshold
&& ConstitutesBiome(inRegion, inBiome, BiomeConfiguration.RoomThreshold))
|| (inBiome.IsLiquidBased
&& ConstitutesBiome(inRegion, inBiome, BiomeConfiguration.LiquidThreshold))
|| ConstitutesBiome(inRegion, inBiome, BiomeConfiguration.LandThreshold))
{
return inBiome.ID;
}
}

// TODO We might want to log this result here, too, though if so it whould be an INFO log rather than a warning.
return BiomeModel.None.ID;
}

// Determines the number of individual parquets that are present inside Rooms in the given MapRegion.
// inRegion -> The region to consider.
// Returns the number of parquets that are part of a known Room.
static ModelID GetParquetsInRooms(MapRegion inRegion)
{
var parquetsInRoom = 0;

// TODO This might be a good place to optimise.
for (var y = 0; y < inRegion.ParquetDefinitions.Rows; y++)
{
for (var x = 0; x < inRegion.ParquetDefinitions.Columns; x++)
{
if (inRegion.Rooms.Any(room => room.ContainsPosition(new Vector2D(x, y))))
{
// Note that we are counting every parquet, including collectibles.
parquetsInRoom += inRegion.ParquetDefinitions[y, x].Count;
}
}
}

return parquetsInRoom;
}

// Determines if the given region has enough parquets contributing to the given biome to exceed the given threshold.
// inRegion -> The region to test.
// inBiome -> The biome to test against.
// inThreshold -> A total number of parquets that must be met for the region to qualify.
// Returns true if enough parquets contribute to the biome, false otherwise.
static bool ConstitutesBiome(MapRegion inRegion, BiomeModel inBiome, int inThreshold)
{
foreach (ModelTag biomeTag in inBiome.ParquetCriteria)
{
if (CountMeetsOrExceedsThreshold(inRegion, parquet => parquet.AddsToBiome == biomeTag, inThreshold))
{
return true;
}
}
return false;
}

// Determines if the region has enough parquets satisfying the given predicate to meet or exceed the given threshold.
// inRegion -> The region to test.
// inPredicate -> A predicate indicating if the parquet should be counted.
// inThreshold -> A total number of parquets that must be met for the region to qualify.
// Returns true if enough parquets satisfy the conditions given, false otherwise.
static bool CountMeetsOrExceedsThreshold(MapRegion inRegion, Predicate<ParquetModel> inPredicate, int inThreshold)
{
var count = 0;

foreach (ParquetStack stack in inRegion.ParquetDefinitions)
{
if (inPredicate(All.Parquets.Get<FloorModel>(stack.Floor)))
{
count++;
}
if (inPredicate(All.Parquets.Get<BlockModel>(stack.Block)))
{
count++;
}
if (inPredicate(All.Parquets.Get<FurnishingModel>(stack.Furnishing)))
{
count++;
}
if (inPredicate(All.Parquets.Get<CollectibleModel>(stack.Collectible)))
{
count++;
}
}

return count >= inThreshold;
}
#endregion
}
#endregion

#region Utilities
/// <summary>
/// Describes the <see cref="MapRegion"/>.
Expand Down

0 comments on commit 7b1e886

Please sign in to comment.