Skip to content

Commit

Permalink
Merge branch 'hotfix-shopping'
Browse files Browse the repository at this point in the history
  • Loading branch information
dymanoid committed Jul 25, 2018
2 parents 51ed51a + 8c1e795 commit b90ea15
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 34 deletions.
9 changes: 3 additions & 6 deletions src/RealTime/CustomAI/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@ internal static class Constants
/// <summary>A distance in game units that corresponds to the complete map.</summary>
public const float FullSearchDistance = BuildingManager.BUILDINGGRID_RESOLUTION * BuildingManager.BUILDINGGRID_CELL_SIZE / 2f;

/// <summary>A chance in percent for an unemployed citizen to stay home until next morning.</summary>
/// <summary>A chance in percent for a citizen to stay home until next scheduled action.</summary>
public const uint StayHomeAllDayChance = 15;

/// <summary>A chance in percent for a citizen to go shopping.</summary>
public const uint GoShoppingChance = 80;

/// <summary>A chance in percent for a citizen to go to sleep when he or she is at home and doesn't go out.</summary>
public const uint GoSleepingChance = 75;
/// <summary>A chance in percent for a citizen to go shopping in the night.</summary>
public const uint NightShoppingChance = 20u;

/// <summary>A chance in percent for a tourist to find a hotel for sleepover.</summary>
public const uint FindHotelChance = 80;
Expand Down
6 changes: 5 additions & 1 deletion src/RealTime/CustomAI/RealTimeResidentAI.Home.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ private bool RescheduleAtHome(ref CitizenSchedule schedule, ref TCitizen citizen
return true;
}

uint goOutChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen));
uint goOutChance = spareTimeBehavior.GetGoOutChance(
CitizenProxy.GetAge(ref citizen),
schedule.WorkShift,
schedule.ScheduledState == ResidentState.Shopping);

if (Random.ShouldOccur(goOutChance))
{
return false;
Expand Down
15 changes: 9 additions & 6 deletions src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal sealed partial class RealTimeResidentAI<TAI, TCitizen>
private bool ScheduleRelaxing(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen)
{
Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen);
if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(citizenAge)) || IsBadWeather())
if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(citizenAge, schedule.WorkShift, false)) || IsBadWeather())
{
return false;
}
Expand Down Expand Up @@ -76,7 +76,7 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin
return;
}

uint relaxChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen));
uint relaxChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen), schedule.WorkShift, false);
ResidentState nextState = Random.ShouldOccur(relaxChance)
? ResidentState.Unknown
: ResidentState.Relaxing;
Expand Down Expand Up @@ -110,8 +110,7 @@ private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen
return false;
}

if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)))
|| !Random.ShouldOccur(GoShoppingChance))
if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen), schedule.WorkShift, true)))
{
return false;
}
Expand Down Expand Up @@ -149,7 +148,7 @@ private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uin
}
else
{
uint moreShoppingChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen));
uint moreShoppingChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen), schedule.WorkShift, true);
ResidentState nextState = Random.ShouldOccur(moreShoppingChance)
? ResidentState.Unknown
: ResidentState.Shopping;
Expand Down Expand Up @@ -243,7 +242,11 @@ private bool RescheduleVisit(ref CitizenSchedule schedule, ref TCitizen citizen,
return true;
}

uint stayChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen));
uint stayChance = spareTimeBehavior.GetGoOutChance(
CitizenProxy.GetAge(ref citizen),
schedule.WorkShift,
schedule.CurrentState == ResidentState.Shopping);

if (!Random.ShouldOccur(stayChance))
{
Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit because of time (see next line for citizen ID)");
Expand Down
7 changes: 6 additions & 1 deletion src/RealTime/CustomAI/RealTimeTouristAI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,12 @@ private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen cit
return;
}

if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather())
uint goOutChance = spareTimeBehavior.GetGoOutChance(
CitizenProxy.GetAge(ref citizen),
WorkShift.Unemployed,
CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods));

if (!Random.ShouldOccur(goOutChance) || IsBadWeather())
{
FindHotel(instance, citizenId, ref citizen);
return;
Expand Down
197 changes: 177 additions & 20 deletions src/RealTime/CustomAI/SpareTimeBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace RealTime.CustomAI
using RealTime.Config;
using RealTime.Simulation;
using RealTime.Tools;
using static Constants;

/// <summary>
/// A class that provides custom logic for the spare time simulation.
Expand All @@ -16,7 +17,11 @@ internal sealed class SpareTimeBehavior
{
private readonly RealTimeConfig config;
private readonly ITimeInfo timeInfo;
private readonly uint[] chances;
private readonly uint[] defaultChances;
private readonly uint[] secondShiftChances;
private readonly uint[] nightShiftChances;
private readonly uint[] shoppingChances;

private float simulationCycle;

/// <summary>Initializes a new instance of the <see cref="SpareTimeBehavior"/> class.</summary>
Expand All @@ -27,7 +32,12 @@ public SpareTimeBehavior(RealTimeConfig config, ITimeInfo timeInfo)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.timeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo));
chances = new uint[Enum.GetValues(typeof(Citizen.AgeGroup)).Length];

int agesCount = Enum.GetValues(typeof(Citizen.AgeGroup)).Length;
defaultChances = new uint[agesCount];
secondShiftChances = new uint[agesCount];
nightShiftChances = new uint[agesCount];
shoppingChances = new uint[agesCount];
}

/// <summary>Sets the duration (in hours) of a full simulation cycle for all citizens.
Expand All @@ -53,8 +63,56 @@ public void RefreshGoOutChances()
weekdayModifier = 1u;
}

bool isWeekend = weekdayModifier > 1u;
float currentHour = timeInfo.CurrentHour;

CalculateDefaultChances(currentHour, weekdayModifier);
CalculateSecondShiftChances(currentHour, isWeekend);
CalculateNightShiftChances(currentHour, isWeekend);
CalculateShoppingChance(currentHour);
}

/// <summary>
/// Gets the probability whether a citizen with specified age would go out on current time.
/// </summary>
///
/// <param name="citizenAge">The age of the citizen to check.</param>
/// <param name="workShift">The citizen's assigned work shift (or <see cref="WorkShift.Unemployed"/>).</param>
/// <param name="needsShopping"><c>true</c> when the citizen needs to buy something; otherwise, <c>false</c>.</param>
///
/// <returns>A percentage value in range of 0..100 that describes the probability whether
/// a citizen with specified age would go out on current time.</returns>
public uint GetGoOutChance(Citizen.AgeGroup citizenAge, WorkShift workShift, bool needsShopping)
{
if (needsShopping)
{
return shoppingChances[(int)citizenAge];
}

int age = (int)citizenAge;
switch (citizenAge)
{
case Citizen.AgeGroup.Young:
case Citizen.AgeGroup.Adult:
switch (workShift)
{
case WorkShift.Second:
return secondShiftChances[age];

case WorkShift.Night:
return nightShiftChances[age];

default:
return defaultChances[age];
}

default:
return defaultChances[age];
}
}

private void CalculateDefaultChances(float currentHour, uint weekdayModifier)
{
float latestGoOutHour = config.GoToSleepUpHour - simulationCycle;
bool isDayTime = currentHour >= config.WakeupHour && currentHour < latestGoOutHour;
float timeModifier;
Expand All @@ -74,33 +132,132 @@ public void RefreshGoOutChances()
timeModifier = 3f / nightDuration * (nightDuration - relativeHour);
}

uint defaultChance = (uint)((timeModifier + weekdayModifier) * timeModifier);
float chance = (timeModifier + weekdayModifier) * timeModifier;
uint roundedChance = (uint)Math.Round(chance);

bool dump = chances[(int)Citizen.AgeGroup.Young] != defaultChance;
#if DEBUG
bool dump = defaultChances[(int)Citizen.AgeGroup.Adult] != roundedChance;
#endif

chances[(int)Citizen.AgeGroup.Child] = isDayTime ? defaultChance : 0;
chances[(int)Citizen.AgeGroup.Teen] = isDayTime ? defaultChance : 0;
chances[(int)Citizen.AgeGroup.Young] = defaultChance;
chances[(int)Citizen.AgeGroup.Adult] = defaultChance;
chances[(int)Citizen.AgeGroup.Senior] = isDayTime ? defaultChance : 0;
defaultChances[(int)Citizen.AgeGroup.Child] = isDayTime ? roundedChance : 0;
defaultChances[(int)Citizen.AgeGroup.Teen] = isDayTime ? (uint)Math.Round(chance * 0.9f) : 0;
defaultChances[(int)Citizen.AgeGroup.Young] = (uint)Math.Round(chance * 1.3f);
defaultChances[(int)Citizen.AgeGroup.Adult] = roundedChance;
defaultChances[(int)Citizen.AgeGroup.Senior] = isDayTime ? (uint)Math.Round(chance * 0.8f) : 0;

#if DEBUG
if (dump)
{
Log.Debug($"GO OUT CHANCES for {timeInfo.Now}: child = {chances[0]}, teen = {chances[1]}, young = {chances[2]}, adult = {chances[3]}, senior = {chances[4]}");
Log.Debug($"DEFAULT GOING OUT CHANCES for {timeInfo.Now}: child = {defaultChances[0]}, teen = {defaultChances[1]}, young = {defaultChances[2]}, adult = {defaultChances[3]}, senior = {defaultChances[4]}");
}
#endif
}

/// <summary>
/// Gets the probability whether a citizen with specified age would go out on current time.
/// </summary>
///
/// <param name="citizenAge">The citizen age to check.</param>
///
/// <returns>A percentage value in range of 0..100 that describes the probability whether
/// a citizen with specified age would go out on current time.</returns>
public uint GetGoOutChance(Citizen.AgeGroup citizenAge)
private void CalculateSecondShiftChances(float currentHour, bool isWeekend)
{
#if DEBUG
uint oldChance = secondShiftChances[(int)Citizen.AgeGroup.Adult];
#endif

float wakeupHour = config.WakeupHour - config.GoToSleepUpHour + 24f;
if (isWeekend || currentHour < config.WakeupHour || currentHour >= wakeupHour)
{
secondShiftChances[(int)Citizen.AgeGroup.Young] = defaultChances[(int)Citizen.AgeGroup.Young];
secondShiftChances[(int)Citizen.AgeGroup.Adult] = defaultChances[(int)Citizen.AgeGroup.Adult];
}
else
{
secondShiftChances[(int)Citizen.AgeGroup.Young] = 0;
secondShiftChances[(int)Citizen.AgeGroup.Adult] = 0;
}

#if DEBUG
if (oldChance != secondShiftChances[(int)Citizen.AgeGroup.Adult])
{
Log.Debug($"SECOND SHIFT GOING OUT CHANCE for {timeInfo.Now}: young = {secondShiftChances[2]}, adult = {secondShiftChances[3]}");
}
#endif
}

private void CalculateNightShiftChances(float currentHour, bool isWeekend)
{
#if DEBUG
uint oldChance = nightShiftChances[(int)Citizen.AgeGroup.Adult];
#endif

float wakeupHour = config.WorkBegin + (config.WakeupHour - config.GoToSleepUpHour + 24f);
if (isWeekend || currentHour < config.WakeupHour || currentHour >= wakeupHour)
{
nightShiftChances[(int)Citizen.AgeGroup.Young] = defaultChances[(int)Citizen.AgeGroup.Young];
nightShiftChances[(int)Citizen.AgeGroup.Adult] = defaultChances[(int)Citizen.AgeGroup.Adult];
}
else
{
nightShiftChances[(int)Citizen.AgeGroup.Young] = 0;
nightShiftChances[(int)Citizen.AgeGroup.Adult] = 0;
}

#if DEBUG
if (oldChance != nightShiftChances[(int)Citizen.AgeGroup.Adult])
{
Log.Debug($"NIGHT SHIFT GOING OUT CHANCE for {timeInfo.Now}: young = {nightShiftChances[2]}, adult = {nightShiftChances[3]}");
}
#endif
}

private void CalculateShoppingChance(float currentHour)
{
return chances[(int)citizenAge];
float minShoppingChanceEndHour = Math.Min(config.WakeupHour, EarliestWakeUp);
float maxShoppingChanceStartHour = Math.Max(config.WorkBegin, config.WakeupHour);
if (minShoppingChanceEndHour == maxShoppingChanceStartHour)
{
minShoppingChanceEndHour = RealTimeMath.Clamp(maxShoppingChanceStartHour - 1f, 2f, maxShoppingChanceStartHour - 1f);
}

#if DEBUG
uint oldChance = shoppingChances[(int)Citizen.AgeGroup.Adult];
#endif

float chance;
bool isNight;
float maxShoppingChanceEndHour = Math.Max(config.GoToSleepUpHour, config.WorkEnd);
if (currentHour < minShoppingChanceEndHour)
{
isNight = true;
chance = NightShoppingChance;
}
else if (currentHour < maxShoppingChanceStartHour)
{
isNight = true;
chance = NightShoppingChance +
((100u - NightShoppingChance) * (currentHour - minShoppingChanceEndHour) / (maxShoppingChanceStartHour - minShoppingChanceEndHour));
}
else if (currentHour < maxShoppingChanceEndHour)
{
isNight = false;
chance = 100;
}
else
{
isNight = true;
chance = NightShoppingChance +
((100u - NightShoppingChance) * (24f - currentHour) / (24f - maxShoppingChanceEndHour));
}

uint roundedChance = (uint)Math.Round(chance);

shoppingChances[(int)Citizen.AgeGroup.Child] = isNight ? 0u : (uint)Math.Round(chance * 0.6f);
shoppingChances[(int)Citizen.AgeGroup.Teen] = isNight ? 0u : roundedChance;
shoppingChances[(int)Citizen.AgeGroup.Young] = roundedChance;
shoppingChances[(int)Citizen.AgeGroup.Adult] = roundedChance;
shoppingChances[(int)Citizen.AgeGroup.Senior] = isNight ? 0u : (uint)Math.Round(chance * 0.8f);

#if DEBUG
if (oldChance != roundedChance)
{
Log.Debug($"SHOPPING CHANCES for {timeInfo.Now}: child = {shoppingChances[0]}, teen = {shoppingChances[1]}, young = {shoppingChances[2]}, adult = {shoppingChances[3]}, senior = {shoppingChances[4]}");
}
#endif
}
}
}

0 comments on commit b90ea15

Please sign in to comment.