Looking over Code

Alivegamer

Member
Joined
Jul 20, 2022
Messages
10
Programming Experience
1-3
I am starting to learn C# and I've created a very simple game you can say and want to get feedback and tips on how to make my code more efficient or something I could do differently and make my code better or tips on some habits I should pick up.
I also don't know if this is a good place to post this.
C#:
using System;

namespace Game
{
    class Menu
    {
        static void Main(string[] args)
        {
            bool loop = true;
            string Input;
            while (loop == true)
            {
                Console.Clear();
                Console.WriteLine("Colony Simulator");
                Console.WriteLine("Start");
                Console.WriteLine("End");
                Input = Console.ReadLine().ToLower();
                if (Input == "start")
                {
                    GamePlay Game = new GamePlay();
                    Game.Start();

                }
                if (Input == "end")
                {
                    loop = false;
                }
            }
        }
    }
    public class GamePlay
    {
        private int Wood;
        private int People;
        private int Rock;
        private int House;
        private int Lumberjack;
        private int Mines;
        private int Miners;
        private int WoodCutters;
        private int Farms;
        private int Farmers;
        private int Food;
        private string Input;

        public GamePlay()
        {
            House = Lumberjack = Mines = Miners = WoodCutters = 0;
            Wood = Rock = Food =  50;
            People = 10;
        }
        public void Job()
        {
            int Working;
            bool loop = true;
            string Input;
            while (loop == true)
            {
                Working = Farmers+Miners+Lumberjack;
                Console.Clear();
                Console.WriteLine("JOBS:\nFarmers: " + Convert.ToString(Farmers) + "\nMiners: " + Convert.ToString(Miners) + "\nLumberjacks: " + Convert.ToString(Lumberjack));
                Console.WriteLine("BACK");
                Console.WriteLine("What job do you want to use?");
                Input = Console.ReadLine().ToLower();
                if (Input == "farmers")
                {
                    Console.Clear();
                    Console.WriteLine("How many farmers?");
                    int amount = Convert.ToInt32(Console.ReadLine());
                    Console.Clear();
                    Console.WriteLine("Plus or minus?");
                    Input = Console.ReadLine().ToLower();
                    if (Input == "plus")
                    {
                        if (amount + Farmers <= Farms * 2 && amount <= People - Working)
                        {
                            Farmers += amount;
                        }
                        else
                        {
                            Console.Clear();
                            Console.WriteLine("You Don't have enough people or farms\nENTER");
                            Console.ReadLine();
                        }
                    }
                    if (Input == "minus")
                    {
                        if (amount <= Farmers)
                        {
                            Farmers -= amount;
                        }
                        else
                        {
                            Console.Clear();
                            Console.WriteLine("You Don't have that much Farmer\nENTER");
                            Console.ReadLine();
                        }
                    }
                }
                if (Input == "miner" || Input == "miners")
                {
                    Console.Clear();
                    Console.WriteLine("How many miners?");
                    int amount = Convert.ToInt32(Console.ReadLine());
                    Console.Clear();
                    Console.WriteLine("Plus or minus?");
                    Input = Console.ReadLine().ToLower();
                    if (Input == "plus")
                    {
                        if (amount + Miners <= Mines * 2 && amount <= People - Working)
                        {
                            Miners += amount;
                        }
                        else
                        {
                            Console.Clear();
                            Console.WriteLine("You Don't have enough people or mines\nENTER");
                            Console.ReadLine();
                        }
                    }
                    if (Input == "minus")
                    {
                        if (amount <= Miners)
                        {
                            Miners -= amount;
                        }
                        else
                        {
                            Console.Clear();
                            Console.WriteLine("You Don't have that much Miners\nENTER");
                            Console.ReadLine();
                        }
                    }
                }
                if (Input == "lumberjack")
                {
                    Console.Clear();
                    Console.WriteLine("How many Lumberjacks?");
                    int amount = Convert.ToInt32(Console.ReadLine());
                    Console.Clear();
                    Console.WriteLine("Plus or minus?");
                    Input = Console.ReadLine().ToLower();
                    if (Input == "plus")
                    {
                        if (amount + Lumberjack <= WoodCutters * 2 && amount <= People - Working)
                        {
                            Lumberjack += amount;
                        }
                        else
                        {
                            Console.Clear();
                            Console.WriteLine("You Don't have enough WoodCutters or people\nENTER");
                            Console.ReadLine();
                        }
                    }
                    if (Input == "minus")
                    {
                        if (amount <= Lumberjack)
                        {
                            Lumberjack -= amount;
                        }
                        else
                        {
                            Console.Clear();
                            Console.WriteLine("You Don't have that much Lumberjacks\nENTER");
                            Console.ReadLine();
                        }
                    }
                }
                if (Input == "back")
                {
                    loop = false;
                }
            } 
        }
        public void Build()
        {
            bool loop = true;
            string Input;
            while (loop == true)
            {
                Console.Clear();
                Console.WriteLine("You Have:\nHouse: " + Convert.ToString(House) + "\nMines: " + Convert.ToString(Mines) + "\nWoodCutters: " + Convert.ToString(WoodCutters));
                Console.WriteLine("Farms: " + Convert.ToString(Farms));
                Console.WriteLine("What do you want to build?");
                Console.WriteLine("House\nMine\nLumberMill\nFarm\n\nBACK");
                Input = Console.ReadLine().ToLower();
                if (Input == "house")
                {
                    Console.Clear();
                    Console.WriteLine("House Cost 5 Wood and 5 Rock\nHow many do you want to build?");
                    int amount = Convert.ToInt32(Console.ReadLine());
                    if (amount*5 <= Wood && amount*5 <= Rock)
                    {
                        Wood -= amount * 5;
                        Rock -= amount *5;
                        House += amount;
                    }
                    else
                    {
                        Console.Clear();
                        Console.WriteLine("You Don't have " + Convert.ToString(amount*5)+ " Wood and/or Rock\nENTER");
                        Console.ReadLine();
                    }
                }
                if (Input == "mine" || Input == "mines")
                {
                    Console.Clear();
                    Console.WriteLine("Mine Cost 10 Rock and 5 Wood\nHow many do you want to build?");
                    int amount = Convert.ToInt32(Console.ReadLine());
                    if (amount * 5 <= Wood && amount * 10 <= Rock)
                    {
                        Rock -= amount * 10;
                        Wood -= amount * 5;
                        Mines += amount;
                    }
                    else
                    {
                        Console.Clear();
                        Console.WriteLine("You dont have " + Convert.ToString(amount*5) + " Wood or " + Convert.ToString(amount*10) + " Rock\nENTER");
                        Console.ReadLine();
                    }
                }
                if (Input == "woodcutter")
                {
                    Console.Clear();
                    Console.WriteLine("WoodCutter Cost 5 Wood and 10 Rock\nHow many do you want to build?");
                    int amount = Convert.ToInt32(Console.ReadLine());
                    if (amount * 5 <= Wood && amount * 10 <= Rock)
                    {
                        Wood -= amount*5;
                        Rock -= amount*10;
                        WoodCutters += amount;
                    }
                    else
                    {
                        Console.Clear();
                        Console.WriteLine("You Don't have " + Convert.ToString(amount * 5) + " Wood or " + Convert.ToString(amount*10) + " Rock\nENTER");
                        Console.ReadLine();
                    }
                }
                if (Input == "farm")
                { 
                    Console.Clear();
                    Console.WriteLine("Farm Cost 10 Wood\nHow many do you want to build?");
                    int amount = Convert.ToInt32(Console.ReadLine());
                    if (amount*10 <=Wood)
                    {
                        Wood -= amount * 10;
                        Farms += amount;
                    }
                    else
                    {
                        Console.Clear();
                        Console.WriteLine("You Dont have " + Convert.ToString(amount * 10) + " Wood\nENTER");
                        Console.ReadLine();
                    }
                }
                if (Input == "back")
                {
                    loop = false;
                }
            }
        }
        public void Start()
        {
            bool loop = true;
            while (loop == true)
            {
                Console.Clear();
                Console.WriteLine("Food " + Convert.ToString(Food) + " " + Convert.ToString((Farmers*5)-People*2) + "\nPeople " + Convert.ToString(People) + "\nWood " + Convert.ToString(Wood) + "\nRock " + Convert.ToString(Rock));
                Console.WriteLine("CAN DO:"); 
                Console.Write("Next Day\nBuild\nJobs\nQuit");
                Input = Console.ReadLine().ToLower();
                if (Input == "next" || Input == "next day")
                {
                    Wood += Lumberjack * 2;
                    Rock += Miners * 2;
                    Food += Farmers * 5;
                    Food -= People * 2;
                    if (Food > 0 && House * 5 > People)
                    {
                        People *= 2;
                        if (People > House * 5)
                        {
                            People = House * 5;
                        }
                    }
                }
                if (Input == "build")
                {
                    Build();
                }
                if (Input == "job")
                {
                    Job();
                }
                if (Input == "quit")
                {
                    loop = false;
                }
                if (Food <= 0)
                {
                    Console.Clear();
                    Console.WriteLine("Your colony starved\nENTER");
                    Console.ReadLine();
                    loop = false;
                }
            }
        }
    }
}
 
This part
C#:
People *= 2;
if (People > House * 5)
{
    People = House * 5;
}
can be replaced with
C#:
People = Math.Min(People * 2, House * 5);
 
That's funny. That was the first thing I changed when I started refactoring the OP's code. I'll be posting my re-write some time tomorrow.
 
My convoluted re-write. I was trying to separate concerns into specific classes.
C#:
using System;

namespace Skydiver.Games.ColonySimulator;

class UserInterface
{
    static void Main()
    {
        ConsoleHelper.ShowMenu("Colony Simulator", "What do you want to do?",
                               exitMenuItem: "eXit",
                               ("Play", () => new UserInterface(new Colony()).RunGameLoop()));

    }

    Colony colony;

    UserInterface(Colony colony)
        => this.colony = colony;

    void ShowStatus()
    {
        Console.WriteLine("Colony Status:");
        foreach (var resource in colony.Resources)
            Console.WriteLine($"{resource.Plural,12}: {resource.Count,3} (Delta: {resource.Forecast,3})");
        foreach (var facility in colony.Facilities)
            Console.WriteLine($"{facility.Plural,12}: {facility.Count,3} (Can build: {facility.BuildMax,3})");
        foreach (var job in colony.Jobs)
            Console.WriteLine($"{job.Plural,12}: {job.Count,3} (Can enlist: {job.EnlistMax,3})");
        Console.WriteLine();
    }

    public void ShowMenu(string title, string prompt, params (string menu, Action action)[] menuItems)
        => ConsoleHelper.ShowMenu(showHeader: ShowStatus, repeat: () => true, title, prompt, "Back", menuItems);

    void BuildFacility(Facility facility)
    {
        Console.WriteLine($"{facility.Plural} costs:");
        if (facility.WoodCost != 0)
            Console.WriteLine($"{facility.WoodCost} Wood");
        if (facility.RockCost != 0)
            Console.WriteLine($"{facility.RockCost} Rock");

        if (facility.BuildMax <= 0)
        {
            Console.WriteLine($"You cannot build any {facility.Plural} right now.");
            return;
        }

        Console.WriteLine($"You can build up to {facility.CountNoun(facility.BuildMax)}");
        int count = ConsoleHelper.PromptInt($"How many {facility.Plural} do you want to build?", 0, facility.BuildMax);
        if (count > 0)
            facility.Build(count);
    }

    void BuildFacilities()
    {
        ShowMenu("Buildings:", "What job do you want to build?",
                 ("House", () => BuildFacility(colony.Houses)),
                 ("Farm", () => BuildFacility(colony.Farms)),
                 ("Mine", () => BuildFacility(colony.Mines)),
                 ("Lumbermill", () => BuildFacility(colony.Lumbermills)));
    }

    void EnlistWorker(Job job)
    {
        if (job.Count != 0)
            Console.WriteLine($"You can remove as many as {job.CountNoun()}.");
        if (job.EnlistMax != 0)
            Console.WriteLine($"You can enlist as many as {job.CountNoun(job.EnlistMax)}.");

        int count = ConsoleHelper.PromptInt($"How many {job.Plural} do you want to remove or enlist? (Enter negative number to remove.)",
                                            -job.Count, job.EnlistMax);
        if (count != 0)
            job.Enlist(count);
    }

    void AssignJobs()
    {
        ShowMenu("Jobs:", "What job do you want to assign?",
                 ("Farmers", () => EnlistWorker(colony.Farmers)),
                 ("Miners", () => EnlistWorker(colony.Miners)),
                 ("Lumberjacks", () => EnlistWorker(colony.Lumberjacks)));
    }

    void NextDay()
    {
        colony.NextDay();
        Console.WriteLine("A new day arises.");
        if (colony.IsStarving)
            Console.WriteLine("Your colony is starving!!!");
    }

    void RunGameLoop()
    {
        ConsoleHelper.ShowMenu(showHeader: ShowStatus,
                               repeat: () => !colony.IsStarving,
                               "Actions:", "What do you want to do?",
                               "Quit",
                               ("Next", NextDay),
                               ("Build", BuildFacilities),
                               ("Jobs", AssignJobs));

        if (colony.IsStarving)
            Console.WriteLine("Your colony starved.");
    }
}

class Colony
{
    public Resource People { get; private init; }
    public Resource Food { get; private init; }
    public Resource Rocks { get; private init; }
    public Resource Wood { get; private init; }
    public IEnumerable<Resource> Resources => new[] { People, Food, Rocks, Wood };

    public Facility Houses { get; private init; }
    public Facility Farms { get; private init; }
    public Facility Mines { get; private init; }
    public Facility Lumbermills { get; private init; }
    public IEnumerable<Facility> Facilities => new[] { Houses, Farms, Mines, Lumbermills };

    public Job Farmers { get; private init; }
    public Job Miners { get; private init; }
    public Job Lumberjacks { get; private init; }
    public IEnumerable<Job> Jobs => new[] { Farmers, Miners, Lumberjacks };

    public int Working => Jobs.Sum(j => j.Count);
    public bool IsStarving => Food.Count < 0;

    public Colony()
    {
        Houses = new Facility(this, Singular: "house", Plural: "houses", WoodCost: 5, RockCost: 5);
        Farms = new Facility(this, Singular: "farm", Plural: "farms", WoodCost: 10, RockCost: 0);
        Mines = new Facility(this, Singular: "mine", Plural: "mines", WoodCost: 5, RockCost: 10);

        Lumbermills = new Facility(this, Singular: "lumbermill", Plural: "lumbermills", WoodCost: 5, RockCost: 10);
        Farmers = new Job(this, Singular: "farmer", Plural: "farmers", Farms);
        Miners = new Job(this, Singular: "miner", Plural: "miners", Mines);
        Lumberjacks = new Job(this, Singular: "lumberjack", Plural: "lumberjacks", Lumbermills);

        People = new Resource(this, "person", "people", 10, PeopleGrowth);
        Food = new Resource(this, "food", "food", 50, () => Farmers.Count * 5 - People.Count * 2);
        Rocks = new Resource(this, "rock", "rocks", 50, () => Miners.Count * 2);
        Wood = new Resource(this, "wood", "wood", 50, () => Lumberjacks.Count * 2);
    }

    int PeopleGrowth()
        => Math.Max(0, Math.Min(Houses.Count * 5, People.Count * 2) - People.Count);

    public void NextDay()
    {
        foreach (var resource in Resources)
            resource.Grow();
    }
}

record class Resource(Colony colony, string Singular, string Plural, int Init, Func<int> Growth)
    : IPluralSingular
{
    public int Count { get; private set; } = Init;
    public int Forecast => Growth();

    public void Grow() => Count += Growth();

    public void Consume(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count), "Value must be not be negative.");
        Count -= count;
    }
}

record class Facility(Colony Colony, string Singular, string Plural, int WoodCost, int RockCost)
    : IPluralSingular
{
    public int Count { get; private set; }

    public int BuildMax
    {
        get
        {
            int canBuild = int.MaxValue;
            if (WoodCost > 0)
                canBuild = Math.Min(canBuild, Colony.Wood.Count / WoodCost);
            if (RockCost > 0)
                canBuild = Math.Min(canBuild, Colony.Rocks.Count / RockCost);
            return canBuild;
        }
    }

    public void Build(int count)
    {
        if (!(0 <= count && count <= BuildMax))
            throw new ArgumentOutOfRangeException(nameof(count), count, $"Value must be between 0 and {BuildMax}");

        Colony.Wood.Consume(count * WoodCost);
        Colony.Rocks.Consume(count * RockCost);
        Count += count;
    }
}

record class Job(Colony Colony, string Singular, string Plural, Facility Facility)
    : IPluralSingular
{
    public int Count { get; private set; }

    public int EnlistMax => Math.Max(0, Math.Min(Facility.Count * 2 - Count, Colony.People.Count - Colony.Working));

    public void Enlist(int delta)
    {
        if (!(-Count <= delta && delta <= EnlistMax))
            throw new ArgumentOutOfRangeException(nameof(delta), delta, $"Value must be between {-Count} and {EnlistMax}");

        Count += delta;
    }
}

static class ConsoleHelper
{
    public static int PromptInt(string prompt, int min, int max)
    {
        if (min > max)
            throw new ArgumentOutOfRangeException(nameof(min), $"Must be <= {nameof(max)}");

        prompt += $" ({min} - {max}) ";
        while (true)
        {
            Console.Write(prompt);
            string input = Console.ReadLine() ?? "";
            if (int.TryParse(input, out int value) && min <= value && value <= max)
                return value;
            Console.WriteLine($"Invalid input. You must enter an integer between {min} and {max}.");
        }
    }

    public static void ShowMenu(string title, string prompt, string exitMenuItem,
                                params (string menu, Action action)[] menuItems)
        => ShowMenu(() => { }, () => true, title, prompt, exitMenuItem, menuItems);

    public static void ShowMenu(Action showHeader, Func<bool> repeat,
                                string title, string prompt, string exitMenuItem,
                                params (string menu, Action action)[] menuItems)
    {
        string input;

        do
        {
            DrawMenu();

            input = Console.ReadLine()?.ToLower() ?? "";
            var (_, action) = menuItems.FirstOrDefault(m => MatchMenuItem(m.menu));
            action?.Invoke();
        } while (!MatchMenuItem(exitMenuItem) && repeat());

        void DrawMenu()
        {
            showHeader();
            Console.WriteLine(title);
            foreach (var (menu, _) in menuItems)
                Console.Write($" {menu}");
            Console.WriteLine($" {exitMenuItem}");
            Console.Write($"{prompt} ");
        }

        bool MatchMenuItem(string menu)
            => input == menu.ToLower() || MatchMenuItemShortcut(menu);

        bool MatchMenuItemShortcut(string menu)
            => input.Length == 1 && char.ToLower(menu.FirstOrDefault(c => char.IsUpper(c))) == input[0];
    }
}

interface IPluralSingular
{
    int Count { get; }
    string Plural { get; }
    string Singular { get; }
}

static class PluralSingularExtensionMethods
{
    public static string CountNoun(this IPluralSingular subject)
        => subject.CountNoun(subject.Count);

    public static string CountNoun(this IPluralSingular subject, int count)
        => count == 1 ? $"1 {subject.Singular}" : $"{count} {subject.Plural}";
}
 
Thank you all for helping me with improving my programming and showing habits I should pick up on. I will copy and paste your improvements Skydiver and compare to my original code thank you.
Looks like i have lots to learn
 
I wasn't happy with the IPluralSingular thing. I added a Noun class with this version:

C#:
using System;

namespace Skydiver.Games.ColonySimulator;

class UserInterface
{
    static void Main()
    {
        ConsoleHelper.ShowMenu("Colony Simulator", "What do you want to do?",
                               exitMenuItem: "eXit",
                               ("Play", () => new UserInterface(new Colony()).RunGameLoop()));
    }

    Colony colony;

    UserInterface(Colony colony)
        => this.colony = colony;

    void ShowStatus()
    {
        Console.WriteLine("Colony Status:");
        foreach (var resource in colony.Resources)
            Console.WriteLine($"{resource.Noun.Plural,12}: {resource.Count,3} (Delta: {resource.Forecast,3})");
        foreach (var facility in colony.Facilities)
            Console.WriteLine($"{facility.Noun.Plural,12}: {facility.Count,3} (Can build: {facility.BuildMax,3})");
        foreach (var job in colony.Workers)
            Console.WriteLine($"{job.Noun.Plural,12}: {job.Count,3} (Can enlist: {job.EnlistMax,3})");
        Console.WriteLine();
    }

    public void ShowMenu(string title, string prompt, params (string menu, Action action)[] menuItems)
        => ConsoleHelper.ShowMenu(showHeader: ShowStatus, repeat: () => true, title, prompt, "Back", menuItems);

    void BuildFacility(Facility facility)
    {
        Console.WriteLine($"{facility.Noun.Plural} costs:");
        if (facility.WoodCost != 0)
            Console.WriteLine($"{colony.Wood.Noun.ToString(facility.WoodCost)}");
        if (facility.RocksCost != 0)
            Console.WriteLine($"{colony.Rocks.Noun.ToString(facility.RocksCost)}");

        if (facility.BuildMax <= 0)
        {
            Console.WriteLine($"You cannot build any {facility.Noun.Plural} right now.");
            return;
        }

        Console.WriteLine($"You can build up to {facility.Noun.ToString(facility.BuildMax)}");
        int count = ConsoleHelper.PromptInt($"How many {facility.Noun.Plural} do you want to build?", 0, facility.BuildMax);
        if (count > 0)
            facility.Build(count);
    }

    void BuildFacilities()
    {
        ShowMenu("Buildings:", "What job do you want to build?",
                 ("House", () => BuildFacility(colony.Houses)),
                 ("Farm", () => BuildFacility(colony.Farms)),
                 ("Mine", () => BuildFacility(colony.Mines)),
                 ("Lumbermill", () => BuildFacility(colony.Lumbermills)));
    }

    void EnlistWorker(Worker job)
    {
        if (job.Count != 0)
            Console.WriteLine($"You can remove as many as {job.Noun.ToString(job.Count)}.");
        if (job.EnlistMax != 0)
            Console.WriteLine($"You can enlist as many as {job.Noun.ToString(job.EnlistMax)}.");

        int count = ConsoleHelper.PromptInt($"How many {job.Noun.Plural} do you want to remove or enlist? (Enter negative number to remove.)",
                                            -job.Count, job.EnlistMax);
        if (count != 0)
            job.Enlist(count);
    }

    void AssignJobs()
    {
        ShowMenu("Jobs:", "What job do you want to assign?",
                 ("Farmers", () => EnlistWorker(colony.Farmers)),
                 ("Miners", () => EnlistWorker(colony.Miners)),
                 ("Lumberjacks", () => EnlistWorker(colony.Lumberjacks)));
    }

    void NextDay()
    {
        colony.NextDay();
        Console.WriteLine("A new day arises.");
        if (colony.IsStarving)
            Console.WriteLine("Your colony is starving!!!");
    }

    void RunGameLoop()
    {
        ConsoleHelper.ShowMenu(showHeader: ShowStatus,
                               repeat: () => !colony.IsStarving,
                               "Actions:", "What do you want to do?",
                               "Quit",
                               ("Next", NextDay),
                               ("Build", BuildFacilities),
                               ("Jobs", AssignJobs));

        if (colony.IsStarving)
            Console.WriteLine("Your colony starved.");
    }
}

class Colony
{
    public Resource Colonists { get; private init; }
    public Resource Food { get; private init; }
    public Resource Rocks { get; private init; }
    public Resource Wood { get; private init; }
    public IEnumerable<Resource> Resources => new[] { Colonists, Food, Rocks, Wood };

    public Facility Houses { get; private init; }
    public Facility Farms { get; private init; }
    public Facility Mines { get; private init; }
    public Facility Lumbermills { get; private init; }
    public IEnumerable<Facility> Facilities => new[] { Houses, Farms, Mines, Lumbermills };

    public Worker Farmers { get; private init; }
    public Worker Miners { get; private init; }
    public Worker Lumberjacks { get; private init; }
    public IEnumerable<Worker> Workers => new[] { Farmers, Miners, Lumberjacks };

    public int Working => Workers.Sum(j => j.Count);
    public bool IsStarving => Food.Count < 0;

    public Colony()
    {
        Houses      = new Facility(this,    "house",        WoodCost: 5,    RocksCost: 5);
        Farms       = new Facility(this,    "farm",         WoodCost: 10,   RocksCost: 0);
        Mines       = new Facility(this,    "mine",         WoodCost: 5,    RocksCost: 10);
        Lumbermills = new Facility(this,    "lumbermill",   WoodCost: 5,    RocksCost: 10);

        Farmers     = new Worker(this,      "farmer",       Farms);
        Miners      = new Worker(this,      "miner",        Mines);
        Lumberjacks = new Worker(this,      "lumberjack",   Lumbermills);

        Colonists   = new Resource(this,    "colonist",             10,     ColonistsGrowth);
        Food        = new Resource(this,    ("food", "food"),       50,     () => Farmers.Count * 5 - Colonists.Count * 2);
        Rocks       = new Resource(this,    "rock",                 50,     () => Miners.Count * 2);
        Wood        = new Resource(this,    ("wood", "wood"),       50,     () => Lumberjacks.Count * 2);
    }

    int ColonistsGrowth()
        => Math.Max(0, Math.Min(Houses.Count * 5, Colonists.Count * 2) - Colonists.Count);

    public void NextDay()
    {
        foreach (var resource in Resources)
            resource.Grow();
    }
}

record class Resource(Colony colony, Noun Noun, int Init, Func<int> Growth)
{
    public int Count { get; private set; } = Init;
    public int Forecast => Growth();

    public void Grow() => Count += Growth();

    public void Consume(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count), "Value must be not be negative.");
        Count -= count;
    }
}

record class Facility(Colony Colony, Noun Noun, int WoodCost, int RocksCost)
{
    public int Count { get; private set; }

    public int BuildMax
    {
        get
        {
            int canBuild = int.MaxValue;
            if (WoodCost > 0)
                canBuild = Math.Min(canBuild, Colony.Wood.Count / WoodCost);
            if (RocksCost > 0)
                canBuild = Math.Min(canBuild, Colony.Rocks.Count / RocksCost);
            return canBuild;
        }
    }

    public void Build(int count)
    {
        if (!(0 <= count && count <= BuildMax))
            throw new ArgumentOutOfRangeException(nameof(count), count, $"Value must be between 0 and {BuildMax}");

        Colony.Wood.Consume(count * WoodCost);
        Colony.Rocks.Consume(count * RocksCost);
        Count += count;
    }
}

record class Worker(Colony Colony, Noun Noun, Facility Facility)
{
    public int Count { get; private set; }

    public int EnlistMax => Math.Max(0, Math.Min(Facility.Count * 2 - Count, Colony.Colonists.Count - Colony.Working));

    public void Enlist(int delta)
    {
        if (!(-Count <= delta && delta <= EnlistMax))
            throw new ArgumentOutOfRangeException(nameof(delta), delta, $"Value must be between {-Count} and {EnlistMax}");

        Count += delta;
    }
}

static class ConsoleHelper
{
    public static int PromptInt(string prompt, int min, int max)
    {
        if (min > max)
            throw new ArgumentOutOfRangeException(nameof(min), $"Must be <= {nameof(max)}");

        prompt += $" ({min} - {max}) ";
        while (true)
        {
            Console.Write(prompt);
            string input = Console.ReadLine() ?? "";
            if (int.TryParse(input, out int value) && min <= value && value <= max)
                return value;
            Console.WriteLine($"Invalid input. You must enter an integer between {min} and {max}.");
        }
    }

    public static void ShowMenu(string title, string prompt, string exitMenuItem,
                                params (string menu, Action action)[] menuItems)
        => ShowMenu(() => { }, () => true, title, prompt, exitMenuItem, menuItems);

    public static void ShowMenu(Action showHeader, Func<bool> repeat,
                                string title, string prompt, string exitMenuItem,
                                params (string menu, Action action)[] menuItems)
    {
        string input;

        do
        {
            DrawMenu();

            input = Console.ReadLine()?.ToLower() ?? "";
            var (_, action) = menuItems.FirstOrDefault(m => MatchMenuItem(m.menu));
            action?.Invoke();
        } while (!MatchMenuItem(exitMenuItem) && repeat());

        void DrawMenu()
        {
            showHeader();
            Console.WriteLine(title);
            foreach (var (menu, _) in menuItems)
                Console.Write($" {menu}");
            Console.WriteLine($" {exitMenuItem}");
            Console.Write($"{prompt} ");
        }

        bool MatchMenuItem(string menu)
            => input == menu.ToLower() || MatchMenuItemShortcut(menu);

        bool MatchMenuItemShortcut(string menu)
            => input.Length == 1 && char.ToLower(menu.FirstOrDefault(c => char.IsUpper(c))) == input[0];
    }
}

record Noun(string Singular, string Plural)
{
    public Noun(string noun)
        : this(noun, noun + (ShouldUseEs(noun) ? "es" : "s"))
    {
    }

    readonly static string[] Endings = new[] { "s", "z", "x", "sh", "ch" };

    static bool ShouldUseEs(string noun)
        => Endings.Any(e => noun.ToLower().EndsWith(e));

    public static implicit operator Noun(string noun)
        => new Noun(noun);

    public static implicit operator Noun((string singular, string plural) noun)
        => new Noun(noun.singular, noun.plural);

    public string ToString(int count)
        => count == 1 ? $"1 {Singular}" : $"{count} {Plural}";
}
 
I wasn't happy with the IPluralSingular thing. I added a Noun class with this version:

C#:
using System;

namespace Skydiver.Games.ColonySimulator;

class UserInterface
{
    static void Main()
    {
        ConsoleHelper.ShowMenu("Colony Simulator", "What do you want to do?",
                               exitMenuItem: "eXit",
                               ("Play", () => new UserInterface(new Colony()).RunGameLoop()));
    }

    Colony colony;

    UserInterface(Colony colony)
        => this.colony = colony;

    void ShowStatus()
    {
        Console.WriteLine("Colony Status:");
        foreach (var resource in colony.Resources)
            Console.WriteLine($"{resource.Noun.Plural,12}: {resource.Count,3} (Delta: {resource.Forecast,3})");
        foreach (var facility in colony.Facilities)
            Console.WriteLine($"{facility.Noun.Plural,12}: {facility.Count,3} (Can build: {facility.BuildMax,3})");
        foreach (var job in colony.Workers)
            Console.WriteLine($"{job.Noun.Plural,12}: {job.Count,3} (Can enlist: {job.EnlistMax,3})");
        Console.WriteLine();
    }

    public void ShowMenu(string title, string prompt, params (string menu, Action action)[] menuItems)
        => ConsoleHelper.ShowMenu(showHeader: ShowStatus, repeat: () => true, title, prompt, "Back", menuItems);

    void BuildFacility(Facility facility)
    {
        Console.WriteLine($"{facility.Noun.Plural} costs:");
        if (facility.WoodCost != 0)
            Console.WriteLine($"{colony.Wood.Noun.ToString(facility.WoodCost)}");
        if (facility.RocksCost != 0)
            Console.WriteLine($"{colony.Rocks.Noun.ToString(facility.RocksCost)}");

        if (facility.BuildMax <= 0)
        {
            Console.WriteLine($"You cannot build any {facility.Noun.Plural} right now.");
            return;
        }

        Console.WriteLine($"You can build up to {facility.Noun.ToString(facility.BuildMax)}");
        int count = ConsoleHelper.PromptInt($"How many {facility.Noun.Plural} do you want to build?", 0, facility.BuildMax);
        if (count > 0)
            facility.Build(count);
    }

    void BuildFacilities()
    {
        ShowMenu("Buildings:", "What job do you want to build?",
                 ("House", () => BuildFacility(colony.Houses)),
                 ("Farm", () => BuildFacility(colony.Farms)),
                 ("Mine", () => BuildFacility(colony.Mines)),
                 ("Lumbermill", () => BuildFacility(colony.Lumbermills)));
    }

    void EnlistWorker(Worker job)
    {
        if (job.Count != 0)
            Console.WriteLine($"You can remove as many as {job.Noun.ToString(job.Count)}.");
        if (job.EnlistMax != 0)
            Console.WriteLine($"You can enlist as many as {job.Noun.ToString(job.EnlistMax)}.");

        int count = ConsoleHelper.PromptInt($"How many {job.Noun.Plural} do you want to remove or enlist? (Enter negative number to remove.)",
                                            -job.Count, job.EnlistMax);
        if (count != 0)
            job.Enlist(count);
    }

    void AssignJobs()
    {
        ShowMenu("Jobs:", "What job do you want to assign?",
                 ("Farmers", () => EnlistWorker(colony.Farmers)),
                 ("Miners", () => EnlistWorker(colony.Miners)),
                 ("Lumberjacks", () => EnlistWorker(colony.Lumberjacks)));
    }

    void NextDay()
    {
        colony.NextDay();
        Console.WriteLine("A new day arises.");
        if (colony.IsStarving)
            Console.WriteLine("Your colony is starving!!!");
    }

    void RunGameLoop()
    {
        ConsoleHelper.ShowMenu(showHeader: ShowStatus,
                               repeat: () => !colony.IsStarving,
                               "Actions:", "What do you want to do?",
                               "Quit",
                               ("Next", NextDay),
                               ("Build", BuildFacilities),
                               ("Jobs", AssignJobs));

        if (colony.IsStarving)
            Console.WriteLine("Your colony starved.");
    }
}

class Colony
{
    public Resource Colonists { get; private init; }
    public Resource Food { get; private init; }
    public Resource Rocks { get; private init; }
    public Resource Wood { get; private init; }
    public IEnumerable<Resource> Resources => new[] { Colonists, Food, Rocks, Wood };

    public Facility Houses { get; private init; }
    public Facility Farms { get; private init; }
    public Facility Mines { get; private init; }
    public Facility Lumbermills { get; private init; }
    public IEnumerable<Facility> Facilities => new[] { Houses, Farms, Mines, Lumbermills };

    public Worker Farmers { get; private init; }
    public Worker Miners { get; private init; }
    public Worker Lumberjacks { get; private init; }
    public IEnumerable<Worker> Workers => new[] { Farmers, Miners, Lumberjacks };

    public int Working => Workers.Sum(j => j.Count);
    public bool IsStarving => Food.Count < 0;

    public Colony()
    {
        Houses      = new Facility(this,    "house",        WoodCost: 5,    RocksCost: 5);
        Farms       = new Facility(this,    "farm",         WoodCost: 10,   RocksCost: 0);
        Mines       = new Facility(this,    "mine",         WoodCost: 5,    RocksCost: 10);
        Lumbermills = new Facility(this,    "lumbermill",   WoodCost: 5,    RocksCost: 10);

        Farmers     = new Worker(this,      "farmer",       Farms);
        Miners      = new Worker(this,      "miner",        Mines);
        Lumberjacks = new Worker(this,      "lumberjack",   Lumbermills);

        Colonists   = new Resource(this,    "colonist",             10,     ColonistsGrowth);
        Food        = new Resource(this,    ("food", "food"),       50,     () => Farmers.Count * 5 - Colonists.Count * 2);
        Rocks       = new Resource(this,    "rock",                 50,     () => Miners.Count * 2);
        Wood        = new Resource(this,    ("wood", "wood"),       50,     () => Lumberjacks.Count * 2);
    }

    int ColonistsGrowth()
        => Math.Max(0, Math.Min(Houses.Count * 5, Colonists.Count * 2) - Colonists.Count);

    public void NextDay()
    {
        foreach (var resource in Resources)
            resource.Grow();
    }
}

record class Resource(Colony colony, Noun Noun, int Init, Func<int> Growth)
{
    public int Count { get; private set; } = Init;
    public int Forecast => Growth();

    public void Grow() => Count += Growth();

    public void Consume(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count), "Value must be not be negative.");
        Count -= count;
    }
}

record class Facility(Colony Colony, Noun Noun, int WoodCost, int RocksCost)
{
    public int Count { get; private set; }

    public int BuildMax
    {
        get
        {
            int canBuild = int.MaxValue;
            if (WoodCost > 0)
                canBuild = Math.Min(canBuild, Colony.Wood.Count / WoodCost);
            if (RocksCost > 0)
                canBuild = Math.Min(canBuild, Colony.Rocks.Count / RocksCost);
            return canBuild;
        }
    }

    public void Build(int count)
    {
        if (!(0 <= count && count <= BuildMax))
            throw new ArgumentOutOfRangeException(nameof(count), count, $"Value must be between 0 and {BuildMax}");

        Colony.Wood.Consume(count * WoodCost);
        Colony.Rocks.Consume(count * RocksCost);
        Count += count;
    }
}

record class Worker(Colony Colony, Noun Noun, Facility Facility)
{
    public int Count { get; private set; }

    public int EnlistMax => Math.Max(0, Math.Min(Facility.Count * 2 - Count, Colony.Colonists.Count - Colony.Working));

    public void Enlist(int delta)
    {
        if (!(-Count <= delta && delta <= EnlistMax))
            throw new ArgumentOutOfRangeException(nameof(delta), delta, $"Value must be between {-Count} and {EnlistMax}");

        Count += delta;
    }
}

static class ConsoleHelper
{
    public static int PromptInt(string prompt, int min, int max)
    {
        if (min > max)
            throw new ArgumentOutOfRangeException(nameof(min), $"Must be <= {nameof(max)}");

        prompt += $" ({min} - {max}) ";
        while (true)
        {
            Console.Write(prompt);
            string input = Console.ReadLine() ?? "";
            if (int.TryParse(input, out int value) && min <= value && value <= max)
                return value;
            Console.WriteLine($"Invalid input. You must enter an integer between {min} and {max}.");
        }
    }

    public static void ShowMenu(string title, string prompt, string exitMenuItem,
                                params (string menu, Action action)[] menuItems)
        => ShowMenu(() => { }, () => true, title, prompt, exitMenuItem, menuItems);

    public static void ShowMenu(Action showHeader, Func<bool> repeat,
                                string title, string prompt, string exitMenuItem,
                                params (string menu, Action action)[] menuItems)
    {
        string input;

        do
        {
            DrawMenu();

            input = Console.ReadLine()?.ToLower() ?? "";
            var (_, action) = menuItems.FirstOrDefault(m => MatchMenuItem(m.menu));
            action?.Invoke();
        } while (!MatchMenuItem(exitMenuItem) && repeat());

        void DrawMenu()
        {
            showHeader();
            Console.WriteLine(title);
            foreach (var (menu, _) in menuItems)
                Console.Write($" {menu}");
            Console.WriteLine($" {exitMenuItem}");
            Console.Write($"{prompt} ");
        }

        bool MatchMenuItem(string menu)
            => input == menu.ToLower() || MatchMenuItemShortcut(menu);

        bool MatchMenuItemShortcut(string menu)
            => input.Length == 1 && char.ToLower(menu.FirstOrDefault(c => char.IsUpper(c))) == input[0];
    }
}

record Noun(string Singular, string Plural)
{
    public Noun(string noun)
        : this(noun, noun + (ShouldUseEs(noun) ? "es" : "s"))
    {
    }

    readonly static string[] Endings = new[] { "s", "z", "x", "sh", "ch" };

    static bool ShouldUseEs(string noun)
        => Endings.Any(e => noun.ToLower().EndsWith(e));

    public static implicit operator Noun(string noun)
        => new Noun(noun);

    public static implicit operator Noun((string singular, string plural) noun)
        => new Noun(noun.singular, noun.plural);

    public string ToString(int count)
        => count == 1 ? $"1 {Singular}" : $"{count} {Plural}";
}
Thank you Skydiver for the improved version of my program. It has gave me an idea of house use classes and showed me new things I didn't know. I am still looking into some things i don't understand still but this is really useful. Thank you
 
Back
Top Bottom