{"id":15593219,"url":"https://github.com/igorocampos/mastermind","last_synced_at":"2025-03-29T10:13:28.544Z","repository":{"id":206330847,"uuid":"261321613","full_name":"igorocampos/Mastermind","owner":"igorocampos","description":".NET Core version of 70's board game - Mastermind","archived":false,"fork":false,"pushed_at":"2020-05-05T02:27:08.000Z","size":23,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-04T08:44:14.710Z","etag":null,"topics":["csharp","mastermind","mastermind-board-game","mastermind-game","netcore"],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igorocampos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2020-05-04T23:54:46.000Z","updated_at":"2020-05-05T02:31:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"adc97b92-8520-409a-94bd-d220c81e5c7b","html_url":"https://github.com/igorocampos/Mastermind","commit_stats":null,"previous_names":["igorocampos/mastermind"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorocampos%2FMastermind","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorocampos%2FMastermind/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorocampos%2FMastermind/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorocampos%2FMastermind/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igorocampos","download_url":"https://codeload.github.com/igorocampos/Mastermind/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246168110,"owners_count":20734390,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","mastermind","mastermind-board-game","mastermind-game","netcore"],"created_at":"2024-10-03T00:05:31.423Z","updated_at":"2025-03-29T10:13:28.522Z","avatar_url":"https://github.com/igorocampos.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mastermind\n.NET Core version of 70's board game - Mastermind\n\n## Rules\nThe game starts with a combination of 4 colors (not necessarily unique) that is hidden and the goal is the correctly guess it within 10 attempts.\n\nWhenever the player verifies his/her guess, a hint status is displayed on the right side showing black and white pins.\n- A black pin means that you guessed a color in the right position.\n- A white pin means that you guessed the right color but in a different position.\n\nIn other words, if you get 4 white pins, you guessed the 4 colors but all in the wrong positions. And if you get 4 black pins, you have won the game!\n\n## Coding it\n\n### Workflow\nWe are going to have a Menu form that will give the Single player or 2 players options to the user. If the single player is chosen then the game will randomly generate a 4 color combination, otherwise it should enable the user to choose a combination for the other player.\n\nWe will also need a main form where the guesses take part. That form will receive in the constructor the 4 color combination, whether it comes from the user input or randomly generated.\n\nThere will also be some options in the main form like \"Verify combination\", \"Give up\" and \"New game\".\n\n### Design\nThere should be 7 different colors to be used in the combination guesses.\nFor the colored pins we can use rounded panels with background colors and for the black and white ones the same but smaller.\n\nUsing a FlowLayoutPanel we will be able to just add the controls and not worry about X and Y positions, so we will use it for adding new combination and result lines of pins.\n\n## Code\n\n### Enum\nThe first thing that comes to mind is creating an enum for all possible colors\n```cs\npublic enum eColor\n{\n    White,\n    Green,\n    Blue,\n    Red,\n    Yellow,\n    Purple,\n    Pink\n}\n```\n\nLet's place it in a file called `Enums.cs` and we should also create a `ColorFromEnum` method where we could easily get a control background color by the enum value.\n\n```cs\npublic static class Enums\n{\n    public static Color ColorFromEnum(eColor color)\n    {\n        switch (color)\n        {\n            case eColor.White:  return Color.White;\n            case eColor.Green:  return Color.Green;\n            case eColor.Blue:   return Color.Blue;\n            case eColor.Red:    return Color.Red;\n            case eColor.Yellow: return Color.Yellow;\n            case eColor.Purple: return Color.Purple;\n            case eColor.Pink:   return Color.Pink;\n            default: throw new ArgumentException(\"Invalid color code.\", nameof(color));\n        }\n    }\n}\n```\n### Rounded Panel\nNext we create a custom class for our rounded panel. There are plenty examples out there of how to do so, I chose a simple one that draws an arc in the corners of the panel, and I adapted it so the panels are always a perfect circle.\n\n```cs\npublic class CirclePanel : Panel\n{\n    protected override void OnPaint(PaintEventArgs e)\n    {\n        base.OnPaint(e);\n        ExtendedDraw(e);\n        DrawBorder(e.Graphics);\n    }\n\n    private Rectangle GetLeftUpper(int e)\n        =\u003e new Rectangle(0, 0, e, e);\n\n    private Rectangle GetRightUpper(int e)\n        =\u003e new Rectangle(Width - e, 0, e, e);\n\n    private Rectangle GetRightLower(int e)\n        =\u003e new Rectangle(Width - e, Height - e, e, e);\n\n    private Rectangle GetLeftLower(int e)\n        =\u003e new Rectangle(0, Height - e, e, e);\n\n    private void ExtendedDraw(PaintEventArgs e)\n    {\n        e.Graphics.SmoothingMode = SmoothingMode.HighQuality;\n        GraphicsPath path = new GraphicsPath();\n        path.StartFigure();\n        path.AddArc(GetLeftUpper(Width), 180, 90);\n        path.AddLine(Width, 0, 0, 0);\n        path.AddArc(GetRightUpper(Width), 270, 90);\n        path.AddLine(Width, Width, Width, Height - Width);\n        path.AddArc(GetRightLower(Width), 0, 90);\n        path.AddLine(0, Height, Width, Height);\n        path.AddArc(GetLeftLower(Width), 90, 90);\n        path.AddLine(0, Height - Width, 0, Width);\n        path.CloseFigure();\n        Region = new Region(path);\n    }\n\n    private void DrawSingleBorder(Graphics graphics)\n    {\n        var pen = new Pen(Color.Transparent, 2.0F);\n        graphics.DrawArc(pen, new Rectangle(0, 0, Width, Width), 180, 90);\n        graphics.DrawArc(pen, new Rectangle(-1, -1, Width, Width), 270, 90);\n        graphics.DrawArc(pen, new Rectangle(-1, Height - Width - 1, Width, Width), 0, 90);\n        graphics.DrawArc(pen, new Rectangle(0, Height - Width - 1, Width, Width), 90, 90);\n        graphics.DrawRectangle(pen, 0.0F, 0.0F, Convert.ToSingle((Width - 1)), Convert.ToSingle((Height - 1)));\n    }\n\n    private void DrawBorder(Graphics graphics)\n        =\u003e DrawSingleBorder(graphics);\n}\n```\n\nAnd now, we can add a `CurrentColor` property that will be very useful to us\n```cs\nprivate eColor currentColor;\n\npublic eColor CurrentColor\n{\n    get =\u003e currentColor;\n    set\n    {\n        currentColor = value;\n        this.BackColor = Enums.ColorFromEnum(currentColor);\n    }\n}\n```\nNotice that whenever you set the property to a enum value, automatically the panel's background color is also set.\n\nIn addition to that, we wish to change the panel color when it gets clicked (or double-clicked), so it can iterate over the enum colors. Let's create a `Panel_Click` method for that.\n\n```cs\nprivate void Panel_Click(object sender, EventArgs e)\n{\n    var cp = (CirclePanel)sender;\n\n    //the next color after pink, is the first color\n    if (cp.CurrentColor == eColor.Pink)\n        cp.CurrentColor = 0;\n    else\n        cp.CurrentColor++;\n}\n```\nWe can now do some initial setting in the class constructor, so the events are assigned and we initiate with the enum's first color.\n```cs\npublic CirclePanel()\n{\n    this.CurrentColor = 0;\n    this.Click += Panel_Click;\n    this.DoubleClick += Panel_Click;\n}\n```\nFor last, we also want to create something that will prevent the color change of the panel.\n- Because the black and white pins don't change colors.\n- And because we don't want the user to be able to change an already verified combination.\n\nWe can achieve that by creating a property named `AllowColorChange` \n```cs\npublic bool AllowColorChange { get; set; } = true;\n```\nand modifying the `Panel_Click` method by inserting this `if` in its first line\n```cs\nif (!this.AllowColorChange)\n    return;\n```\n\n### Main form\nFor better understanding, this is the looks of the form\n\n![](frmMain.png)\n\nAs I said before, this form will receive in the constructor the 4 color combination that is the answer to the puzzle. And we will add it in the `grpAnswer` groupbox, behind the `pnlHide` panel.\nOnce the game ends, either by winning or losing it, `pnlHide` is removed and the correct answer revealed.\n\nWe can start by creating some useful fields in this form\n```cs\nprivate int remainingChances = 10;\nprivate readonly List\u003cCirclePanel\u003e currentColors = new List\u003cCirclePanel\u003e();\nprivate readonly List\u003ceColor\u003e answer = new List\u003ceColor\u003e();\n```\n- `remainingChances` will be used to keep track of when the game needs to end.\n- `currentColors` will keep track of the combination that the user just inserted.\n- `answer` will save the enum values for the answer, in the correct order.\n\nNow, whenever `btnVerify` is pressed we want a new row of guesses to be added to flpLayout. Let's do that by creating a method that\n- Disable all pins in `currentColors` list, so the user can't change its color again.\n- Insert new `CirclePanel`s in the list and in the UI.\n- Insert a separation line for better looks.\n```cs\nprivate void AddNewRow()\n{\n    foreach (var pin in currentColors)\n        pin.AllowColorChange = false;\n\n    currentColors.Clear();\n    for (int i = 0; i \u003c 4; i++)\n    {\n        var coloredPin = new CirclePanel { Size = new Size(20, 20) };\n        flpLayout.Controls.Add(coloredPin);\n        currentColors.Add(coloredPin);\n    }\n\n    //Adds a separation line made with a simple Panel\n    flpLayout.Controls.Add(GetNewLine(flpLayout.Width - 5));\n}\n\nprivate Panel GetNewLine(int width)\n{\n    return new Panel\n    {\n        BackColor = Color.LightGray,\n        Size = new Size(width, 1),\n        Margin = new Padding(2, 2, 2, 2),\n    };\n}\n```\nFollowing the same logic, we also want a new result row, but this will be a little more sophisticated, because we need the number of black and white pins. We first add the black pins (if any) and after the white pins. Transparent pins will help us keep the space in case less than 4 pins were inserted.\n\n```cs\nprivate void AddNewResultPin(Color color)\n{\n    var pin = new CirclePanel\n    {\n        Size = new Size(10, 10),\n        Margin = new Padding(1, 2, 1, 1),\n        BackColor = color,\n        AllowColorChange = false\n    };\n    flpResults.Controls.Add(pin);\n}\n\nprivate void AddNewResultRow(int blackPins, int whitePins)\n{\n    if (blackPins + whitePins \u003e 4)\n        throw new Exception(\"Sum of white and black pins should be at most 4.\");\n\n    int count = 0;\n\n    //add all black pins\n    while (count \u003c blackPins)\n    {\n        AddNewResultPin(Color.Black);\n        count++;\n    }\n\n    //add all white pins\n    while (count \u003c whitePins + blackPins)\n    {\n        AddNewResultPin(Color.White);\n        count++;\n    }\n\n    //add remaining transparent pins\n    while (count \u003c 4)\n    {\n        AddNewResultPin(Color.Transparent);\n        count++;\n    }\n\n    flpResults.Controls.Add(GetNewLine(flpResults.Width - 5));\n}\n```\nNow the **core** logic of this game, the determination of how many black and white pins a guess gets.\n\nFirst we compare our `currentColors` panel list with the actual `answer` to find all black pins. We need a `remainingColors` list here, because the white pin can't be referenced to a color that already got a black pin. So if the current position, that is not one already used by a black pin (`array[i] == 0`), is the same as any remaining color (no matter which position), we add a white pin and remove that color from the list.\n\n```cs\nprivate (int, int) GetResults()\n{\n    int[] array = new int[] { 0, 0, 0, 0 };\n    List\u003ceColor\u003e remainingColors = new List\u003ceColor\u003e();\n    (int blacks, int whites) result = (0, 0);\n    \n    //set all black pins\n    for (int i = 0; i \u003c 4; i++)\n    {\n        if (currentColors[i].CurrentColor == answer[i])\n        {\n            array[i] = 1;\n            result.blacks++;\n        }\n        else\n            remainingColors.Add(answer[i]);\n    }\n\n    //set all white pins (a white can't be compared to a position that is already black)\n    for (int i = 0; i \u003c 4; i++)\n    {\n        var color = currentColors[i].CurrentColor;\n\n        //If it's not set yet, search for other positions with the same color\n        if (array[i] == 0)\n        {\n            int index = remainingColors.IndexOf(color);\n\n            //Found color in different position\n            if (index \u003e= 0)\n            {\n                remainingColors.RemoveAt(index);\n                result.whites++;\n            }\n        }\n    }\n    return result;\n}\n```\nNow we can code our `btnVerify_Click`\n\n```cs\nprivate void btnVerify_Click(object sender, EventArgs e)\n{\n    remainingChances--;\n    btnVerify.Text = $\"Verify{Environment.NewLine}({remainingChances} left)\";\n    var (blackPins, whitePins) = GetResults();\n    AddNewResultRow(blackPins, whitePins);\n\n    //Is the game finished?\n    if (remainingChances == 0 || blackPins == 4)\n        pnlHide.Visible = btnVerify.Enabled = false;\n    else\n        AddNewRow();\n}\n```\n\nFor last we can initialize what we need in the constructor\n```cs\npublic frmMain(eColor color1, eColor color2, eColor color3, eColor color4)\n{\n    InitializeComponent();\n\n    //Starts with a new guessing row\n    AddNewRow();\n    answer.AddRange(new[] { color1, color2, color3, color4 });\n\n    var answerControls = new[] {\n        new CirclePanel { Size = new Size(20, 20), Location = new Point(15,20), CurrentColor = answer[0] },\n        new CirclePanel { Size = new Size(20, 20), Location = new Point(38,20), CurrentColor = answer[1] },\n        new CirclePanel { Size = new Size(20, 20), Location = new Point(61,20), CurrentColor = answer[2] },\n        new CirclePanel { Size = new Size(20, 20), Location = new Point(84,20), CurrentColor = answer[3] }\n    };\n\n    grpAnswer.Controls.AddRange(answerControls);\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figorocampos%2Fmastermind","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figorocampos%2Fmastermind","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figorocampos%2Fmastermind/lists"}