diff --git a/StackEgg.sln b/StackEgg.sln new file mode 100644 index 0000000..a1138c6 --- /dev/null +++ b/StackEgg.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30723.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackEgg", "StackEgg\StackEgg.csproj", "{DA42D659-D5F3-47D8-958E-0555A499B40A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DA42D659-D5F3-47D8-958E-0555A499B40A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA42D659-D5F3-47D8-958E-0555A499B40A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA42D659-D5F3-47D8-958E-0555A499B40A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA42D659-D5F3-47D8-958E-0555A499B40A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/StackEgg/App.config b/StackEgg/App.config new file mode 100644 index 0000000..fad249e --- /dev/null +++ b/StackEgg/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/StackEgg/Program.cs b/StackEgg/Program.cs new file mode 100644 index 0000000..0f8b765 --- /dev/null +++ b/StackEgg/Program.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StackEgg +{ + class Program + { + static void Main(string[] args) + { + var game = new StackEggGame().Initialize(); + + while (true) + { + if (game.CurrentPhase == null) + { + switch (game.CurrentPhaseId) + { + case StackEggPhase.Id.WonTheInternet: + Console.WriteLine("Congratulations, your site has won the internet!"); + break; + case StackEggPhase.Id.Failed: + Console.WriteLine("Oops, your site was shut down."); + break; + } + return; + } + + var sb = new StringBuilder(); + sb.AppendFormat("{0} day{1} elapsed, your Site is ", game.DaysElapsed, game.DaysElapsed == 1 ? "" : "s"); + switch (game.CurrentPhaseId) + { + case StackEggPhase.Id.PrivateBeta: + sb.Append("in private beta"); + break; + case StackEggPhase.Id.PublicBeta: + sb.Append("in public beta"); + break; + case StackEggPhase.Id.Launched: + sb.Append("fully graduated"); + break; + } + sb.AppendFormat(", phase progress: {0}%", (int)(game.GetPhaseProgress() * 100)); + sb.AppendLine(); + sb.AppendLine("Site stats:"); + foreach (var stat in game.CurrentPhase.AvailableStats) + { + var exact = game.Stats[stat]; + var hearts = StackEggGame.HeartRound(exact); + sb.AppendFormat(" {0, -10} {1}{2}", stat, new String('#', hearts), new String('.', 4 - hearts)); + if (StackEggGame.HeartRoundExtended(exact) < 0) + sb.Append(" (dangerously low!)"); + sb.AppendLine(); + } + if (game.ModeratorFlags > 0) + { + sb.AppendLine(); + sb.AppendLine(" Mod flags " + game.ModeratorFlags); + } + + sb.AppendLine(); + if (game.IsFailing) + { + sb.AppendLine("Your Site is on the verge of being shut down!"); + sb.AppendLine("Only flagging for moderator attention can save it now."); + sb.AppendLine(); + } + + sb.AppendLine("Choose your action:"); + + foreach (var action in game.AvailableActions()) + { + sb.AppendFormat("{0}: {1}", (int)action, action); + sb.AppendLine(); + } + sb.AppendLine("q: Quit"); + + Console.Write(sb.ToString()); + + var validAction = false; + StackEggAction chosenAction = StackEggAction.Nothing; + + while (!validAction) + { + var s = Console.ReadLine(); + if (s == "q" || s == "Q") + { + Console.WriteLine("Good bye."); + return; + } + else + { + int num; + if (int.TryParse(s, out num)) + { + chosenAction = (StackEggAction)num; + if (game.ActionIsLegal(chosenAction)) + validAction = true; + } + } + if (!validAction) + Console.WriteLine("That's not a valid action, choose again:"); + } + Console.WriteLine(); + Console.WriteLine(); + game.AdvanceDay(chosenAction); + } + } + } +} diff --git a/StackEgg/Properties/AssemblyInfo.cs b/StackEgg/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0bc9eea --- /dev/null +++ b/StackEgg/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("StackEgg")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Stack Exchange Inc.")] +[assembly: AssemblyProduct("StackEgg")] +[assembly: AssemblyCopyright("Copyright © Stack Exchange Inc. 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1ac6139c-7415-4295-8d9d-488b7db7e0d8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/StackEgg/StackEgg.csproj b/StackEgg/StackEgg.csproj new file mode 100644 index 0000000..dfca08c --- /dev/null +++ b/StackEgg/StackEgg.csproj @@ -0,0 +1,63 @@ + + + + + Debug + AnyCPU + {DA42D659-D5F3-47D8-958E-0555A499B40A} + Exe + Properties + StackEgg + StackEgg + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/StackEgg/StackEggAction.cs b/StackEgg/StackEggAction.cs new file mode 100644 index 0000000..b3e71aa --- /dev/null +++ b/StackEgg/StackEggAction.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StackEgg +{ + public enum StackEggAction + { + Nothing = 0, + + // the follwing five are the "base actions" + Ask, + Answer, + Upvote, + Downvote, + Close, + + FlagForModerator + } +} \ No newline at end of file diff --git a/StackEgg/StackEggGame.cs b/StackEgg/StackEggGame.cs new file mode 100644 index 0000000..9f30497 --- /dev/null +++ b/StackEgg/StackEggGame.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StackEgg +{ + public class StackEggGame + { + public int DaysElapsed { get; set; } + + public StackEggPhase.Id CurrentPhaseId { get; set; } + public StackEggPhase CurrentPhase { get { return StackEggPhase.ById(CurrentPhaseId); } } + + public Dictionary Stats { get; set; } + + public Dictionary AccumulatedStats { get; set; } + + public int ModeratorFlags { get; set; } + + public bool IsFailing { get; set; } + + /// + /// equal to CurrentPhaseId if the game is still active + /// + public StackEggPhase.Id ReachedActivePhaseId { get; set; } + + public StackEggGame Initialize() + { + DaysElapsed = 0; + CurrentPhaseId = ReachedActivePhaseId = StackEggPhase.Id.PrivateBeta; + + Stats = new Dictionary() + { + { StackEggStat.Questions, 0.0 }, + { StackEggStat.Answers, 0.0 }, + { StackEggStat.Quality, 0.0 }, + { StackEggStat.Users, 0.0 }, + { StackEggStat.Traffic, 0.0 } + }; + + AccumulatedStats = new Dictionary() + { + { StackEggStat.Questions, 0 }, + { StackEggStat.Answers, 0 }, + { StackEggStat.Quality, 0 }, + { StackEggStat.Users, 0 }, + { StackEggStat.Traffic, 0 } + }; + + ModeratorFlags = 0; + //AdvanceToNextPhase(); AdvanceToNextPhase(); + + return this; + } + + public static int UsefulRound(double x) + { + return (int)Math.Round(x, MidpointRounding.AwayFromZero); + } + + /// + /// rounds a stat to the number of hearts (zero to four) that should be displayed to the user + /// + public static int HeartRound(double x) + { + return Math.Min(4, Math.Max(0, UsefulRound(x))); + } + + /// + /// Like HeartRound, but can return -1 (which to the user would cause a red "dangerously low" warning) + /// + public static int HeartRoundExtended(double x) + { + return Math.Min(4, Math.Max(-1, UsefulRound(x))); + } + + private static double CapStat(double x, bool atZero) + { + return Math.Min(4.5, Math.Max(atZero ? -0.49 : -1.5, x)); + } + + public IEnumerable AvailableActions() + { + if (CurrentPhase == null) + yield break; + + if (!IsFailing) + { + foreach (var action in CurrentPhase.AvailableActions) + yield return action; + } + + if (ModeratorFlags > 0) + yield return StackEggAction.FlagForModerator; + + yield return StackEggAction.Nothing; + } + + public bool ActionIsLegal(StackEggAction action) + { + if (CurrentPhase == null) + return false; + switch (action) + { + case StackEggAction.Nothing: return true; + case StackEggAction.FlagForModerator: return ModeratorFlags > 0; + default: return !IsFailing && CurrentPhase.AvailableActions.Contains(action); + } + } + + public IEnumerable BadStats() + { + return StatsLessThan(-0.5); + } + public IEnumerable CriticalStats() + { + return StatsLessThan(-1.5); + } + + public IEnumerable StatsLessThan(double x) + { + return StatsAvailableNowOrAtTheEnd().Where(s => Stats[s] <= x); + } + + private void PerformBaseAction(StackEggAction action) + { + var phase = CurrentPhase; + var newValues = new Dictionary(); + foreach (var influencedStat in phase.AvailableStats) + { + var oldValue = Stats[influencedStat]; + var newValue = oldValue; + bool anyInfluence = false, + anyPositiveActionInfluence = false; + var statInfluence = phase.StatInfluences[influencedStat]; + if (statInfluence.ActionInfluences != null && statInfluence.ActionInfluences.ContainsKey(action)) + { + var change = statInfluence.ActionInfluences[action]; + newValue = oldValue + change; + if (change > 0) + { + // see the guarantee in the description of StackEggStatInfluence.ActionInfluences + newValue = Math.Max(newValue, HeartRound(oldValue) + 1); + anyPositiveActionInfluence = true; + } + anyInfluence = true; + } + + if (statInfluence.StatInfluences != null && !anyPositiveActionInfluence) + { + foreach (var kvp in statInfluence.StatInfluences) + { + var influencingStat = kvp.Key; + var arrayIndex = HeartRoundExtended(Stats[influencingStat]) + 1; + var change = kvp.Value[arrayIndex]; + if (change.HasValue) + { + newValue += change.Value; + anyInfluence = true; + } + } + } + + if (!anyInfluence) + { + newValue = oldValue + statInfluence.IdleInfluence; + } + + newValues[influencedStat] = CapStat(newValue, statInfluence.CapAtZero); + } + + foreach (var stat in phase.AvailableStats) + { + Stats[stat] = newValues[stat]; + } + } + + private void FlagForModerator() + { + foreach (var stat in CurrentPhase.AvailableStats) + { + Stats[stat] = Math.Max(Stats[stat], 1); + } + ModeratorFlags--; + } + + /// + /// Returns true if we're advancing to a new phase, false otherwise + /// + /// + /// + public bool AdvanceDay(StackEggAction performedAction) + { + if (!ActionIsLegal(performedAction)) + throw new InvalidOperationException(performedAction + " is not a legal action at this time"); + + var critical = CriticalStats().ToList(); + + switch (performedAction) + { + case StackEggAction.Ask: + case StackEggAction.Answer: + case StackEggAction.Upvote: + case StackEggAction.Downvote: + case StackEggAction.Close: + case StackEggAction.Nothing: + PerformBaseAction(performedAction); + break; + case StackEggAction.FlagForModerator: + FlagForModerator(); + break; + } + + foreach (var stat in CurrentPhase.AvailableStats) + { + AccumulatedStats[stat] += HeartRound(Stats[stat]); + } + + DaysElapsed++; + + var stillCritical = critical.Intersect(CriticalStats()).ToList(); + if (stillCritical.Count > 0) + { + if (!IsFailing && ModeratorFlags > 0) + { + IsFailing = true; + return false; + } + else + { + CurrentPhaseId = StackEggPhase.Id.Failed; + return true; + } + } + + IsFailing = false; + + if (GetPhaseProgress() >= 1.0) + { + AdvanceToNextPhase(); + return true; + } + return false; + } + + public IEnumerable StatsAvailableNowOrAtTheEnd() + { + if (CurrentPhase != null) + return CurrentPhase.AvailableStats; + + return StackEggPhase.ById(ReachedActivePhaseId).AvailableStats; + } + + private void AdvanceToNextPhase() + { + var oldPhase = CurrentPhase; + CurrentPhaseId = oldPhase.NextPhaseId; + + if (CurrentPhase != null) + { + ModeratorFlags++; + ReachedActivePhaseId = CurrentPhaseId; + + // stats that were available in the old phase are set to 1, the others stay at 0 + // (note we're relying on the fact that AvailableStats only gets new stats, but never + // has stats removed) + foreach (var oldStat in oldPhase.AvailableStats) + { + Stats[oldStat] = 1.0; + AccumulatedStats[oldStat] = 0; + } + } + } + + public double GetPhaseProgress() + { + if (CurrentPhase == null) + return 0.0; + + var goals = CurrentPhase.StatGoals; + if (goals == null || goals.Count == 0) + return 0.0; + return goals.Average(kvp => Math.Min(1.0, (double)AccumulatedStats[kvp.Key] / kvp.Value)); + } + } +} \ No newline at end of file diff --git a/StackEgg/StackEggPhase.cs b/StackEgg/StackEggPhase.cs new file mode 100644 index 0000000..ce3db72 --- /dev/null +++ b/StackEgg/StackEggPhase.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StackEgg +{ + public class StackEggPhase + { + public enum Id + { + PrivateBeta = 1, + PublicBeta, + Launched, + + Failed = 98, + WonTheInternet = 99, + } + + /// + /// The stats that are used during this phase + /// + public StackEggStat[] AvailableStats { get; set; } + + /// + /// The base actions available during this phase + /// + public StackEggAction[] AvailableActions { get; set; } + + public Dictionary StatInfluences { get; set; } + + /// + /// The criteria required to reach the next phase. All stat goals must be reached to + /// advance to the next round. The number this is checked against is the user-visible stat + /// (i.e. zero to four hearts), integrated over the days. E.g. two days at two hearts and one + /// day at three hearts gets you seven points closer to the goal. + /// + public Dictionary StatGoals { get; set; } + + /// + /// The phase that the game will advance to when all goals are met. + /// + public Id NextPhaseId { get; set; } + + + public static StackEggPhase ById(Id id) + { + switch (id) + { + case Id.PrivateBeta: return PrivateBeta; + case Id.PublicBeta: return PublicBeta; + case Id.Launched: return Launched; + case Id.WonTheInternet: + case Id.Failed: + return null; + } + throw new ArgumentOutOfRangeException("id"); + } + + + + public static StackEggPhase PrivateBeta = new StackEggPhase + { + AvailableStats = new StackEggStat[] { StackEggStat.Questions, StackEggStat.Answers }, + AvailableActions = new StackEggAction[] { StackEggAction.Ask, StackEggAction.Answer }, + StatInfluences = new Dictionary() + { + { + StackEggStat.Questions, new StackEggStatInfluence + { + // asking increases the question stat + ActionInfluences = new Dictionary() { { StackEggAction.Ask, 1.0 } }, + + StatInfluences = null, + + IdleInfluence = -0.3 + + } + }, + { + StackEggStat.Answers, new StackEggStatInfluence + { + // answering increases the answers stat + ActionInfluences = new Dictionary() { { StackEggAction.Answer, 1.0 } }, + + StatInfluences = new Dictionary() + { + // a low question count causes a decrease in answers + { StackEggStat.Questions, new double?[] { -1.0, -0.5, null, null, null, null } } + }, + + IdleInfluence = -0.3 + } + } + }, + StatGoals = new Dictionary() + { + { StackEggStat.Questions, 50 }, + { StackEggStat.Answers, 50 } + }, + NextPhaseId = Id.PublicBeta + }; // end private beta + + + public static StackEggPhase PublicBeta = new StackEggPhase + { + AvailableStats = new StackEggStat[] { StackEggStat.Questions, StackEggStat.Answers, StackEggStat.Users, StackEggStat.Quality }, + AvailableActions = new StackEggAction[] { StackEggAction.Ask, StackEggAction.Answer, StackEggAction.Upvote, StackEggAction.Downvote }, + StatInfluences = new Dictionary() + { + { + StackEggStat.Questions, new StackEggStatInfluence + { + // asking increases the question stat + ActionInfluences = new Dictionary() { { StackEggAction.Ask, 1.0 } }, + + StatInfluences = null, + + IdleInfluence = -0.3 + + } + }, + { + StackEggStat.Answers, new StackEggStatInfluence + { + // answering increases the answers stat + ActionInfluences = new Dictionary() { { StackEggAction.Answer, 1.0 } }, + + StatInfluences = new Dictionary() + { + // A low question count causes a decrease in answers. These value are a little bit harder than in private beta + { StackEggStat.Questions, new double?[] { -1.0, -0.5, -0.4, null, null, null } } + }, + + IdleInfluence = -0.3 + } + }, + { + StackEggStat.Users, new StackEggStatInfluence + { + // upvoting increases the users stat; downvoting decreases it + ActionInfluences = new Dictionary() { { StackEggAction.Upvote, 1.0 }, { StackEggAction.Downvote, -0.5 } }, + + StatInfluences = null, + + IdleInfluence = -0.3 + } + }, + { + StackEggStat.Quality, new StackEggStatInfluence + { + // downvoting increases the quality stat + ActionInfluences = new Dictionary() { { StackEggAction.Downvote, 1.0 } }, + + StatInfluences = null, + + IdleInfluence = -0.3 + + } + } + }, + StatGoals = new Dictionary() + { + { StackEggStat.Questions, 200 }, + { StackEggStat.Answers, 200 }, + { StackEggStat.Users, 200 }, + { StackEggStat.Quality, 200 } + }, + NextPhaseId = Id.Launched + }; // end public beta + + + public static StackEggPhase Launched = new StackEggPhase + { + AvailableStats = new StackEggStat[] { StackEggStat.Questions, StackEggStat.Answers, StackEggStat.Users, StackEggStat.Quality, StackEggStat.Traffic }, + AvailableActions = new StackEggAction[] { StackEggAction.Ask, StackEggAction.Answer, StackEggAction.Upvote, StackEggAction.Downvote, StackEggAction.Close }, + StatInfluences = new Dictionary() + { + { + StackEggStat.Questions, new StackEggStatInfluence + { + // asking increases the question stat; closing severly decreases it + ActionInfluences = new Dictionary() { { StackEggAction.Ask, 1.0 }, { StackEggAction.Close, -2.0 } }, + + StatInfluences = null, + + IdleInfluence = -0.3 + + } + }, + { + StackEggStat.Answers, new StackEggStatInfluence + { + // answering increases the answers stat + ActionInfluences = new Dictionary() { { StackEggAction.Answer, 1.0 } }, + + StatInfluences = new Dictionary() + { + // A low question count causes a decrease in answers. These value are even harder than in public beta + { StackEggStat.Questions, new double?[] { -1.0, -0.8, -0.6, -0.4, null, null } } + }, + + IdleInfluence = -0.3 + } + }, + { + StackEggStat.Users, new StackEggStatInfluence + { + // upvoting increases the users stat; downvoting decreases it + ActionInfluences = new Dictionary() { { StackEggAction.Upvote, 1.0 }, { StackEggAction.Downvote, -0.5 } }, + + StatInfluences = new Dictionary() + { + // very high traffic brings new users + { StackEggStat.Traffic, new double?[] { null, null, null, -0.2, 0.0, 0.2 } } + }, + + IdleInfluence = -0.3 + } + }, + { + StackEggStat.Quality, new StackEggStatInfluence + { + // downvoting increases the quality stat, closing even more so + ActionInfluences = new Dictionary() { { StackEggAction.Downvote, 1.0 }, { StackEggAction.Close, 2.0 } }, + + StatInfluences = new Dictionary() + { + // very high traffic badly impacts quality + { StackEggStat.Traffic, new double?[] { null, null, null, null, -0.7, -0.9 } } + }, + + IdleInfluence = -0.3 + } + }, + { + StackEggStat.Traffic, new StackEggStatInfluence + { + CapAtZero = true, + + // traffic cannot be directly influenced + ActionInfluences = null, + + StatInfluences = new Dictionary() + { + // both answers and quality have to be pretty high for traffic growth + { StackEggStat.Quality, new double?[] { -0.5, -0.5, -0.5, -0.2, -0.1, 0.1 } }, + { StackEggStat.Answers, new double?[] { -0.3, -0.3, -0.3, 0, 0.1, 0.3 } } + }, + + IdleInfluence = 0 + } + } + }, + StatGoals = new Dictionary() + { + { StackEggStat.Questions, 400 }, + { StackEggStat.Answers, 400 }, + { StackEggStat.Users, 400 }, + { StackEggStat.Quality, 400 }, + { StackEggStat.Traffic, 300 } + }, + NextPhaseId = Id.WonTheInternet + }; // end launched + + } +} \ No newline at end of file diff --git a/StackEgg/StackEggStat.cs b/StackEgg/StackEggStat.cs new file mode 100644 index 0000000..18684f5 --- /dev/null +++ b/StackEgg/StackEggStat.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StackEgg +{ + public enum StackEggStat + { + Questions = 1, + Answers, + Users, + Quality, + Traffic + } +} \ No newline at end of file diff --git a/StackEgg/StackEggStatInfluence.cs b/StackEgg/StackEggStatInfluence.cs new file mode 100644 index 0000000..f55968c --- /dev/null +++ b/StackEgg/StackEggStatInfluence.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StackEgg +{ + /// + /// Specifies how a particular stat is influenced (per day) by the taken action, + /// the other stats, and in absence of other influences. + /// + public class StackEggStatInfluence + { + /// + /// Specifies how the performed action influences the stat. If the influence is positive, + /// then the action is guaranteed to make a *user-visible* change (i.e. the stat is increased + /// such that there is at least one more full heart). Unless of course there are already + /// four full hearts. + /// + public Dictionary ActionInfluences { get; set; } + + /// + /// Specifies how the stat is influenced by the values of other stats. The double?[] must have + /// a length of six, corresponding to -1, 0, 1, 2, 3, 4 hearts. Example: If for the influenced stat + /// StackEggStat.Answers and the influencing stat StackEggStat.Questions, the value + /// is [-1, -.8, -.6, -.4, null, null], and the current questions stat is 1.9: That rounds to 2. + /// Thus the question stat causes a decrease in answer stat by 0.4. + /// + /// StatInfluence is not consulted if the performed action has a positive influence on the influenced stat + /// (i.e. in the above example, answers aren't influenced by questions if the performed action was "Answer"). + /// + public Dictionary StatInfluences { get; set; } + + /// + /// If the performed action does not influence the stat and there is no influence from other stats, then + /// the stat is changed by this amount. + /// + /// "No influence" does *not* mean "zero"; rather it means the ActionInfluence does not contain the action at all, + /// and StatInfluence either doesn't contain anything, or none of the relevant double?s is non-null. + /// + public double IdleInfluence { get; set; } + + /// + /// If this is true, then the stat cannot got to -1 hearts (i.e. no red "dangerously low" warning and the + /// stat cannot cause a site shutdown). + /// + public bool CapAtZero { get; set; } + } +} \ No newline at end of file