Skip to content

Commit bf94a44

Browse files
committed
Initial commit
0 parents  commit bf94a44

14 files changed

+370
-0
lines changed

.github/workflows/build-and-test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Build and Test
2+
3+
on:
4+
push:
5+
pull_request:
6+
branches: [ main ]
7+
paths:
8+
- '**.cs'
9+
- '**.csproj'
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v2
17+
18+
- name: Setup .NET Core
19+
uses: actions/setup-dotnet@v3
20+
with:
21+
dotnet-version: 5.0.x
22+
23+
- name: Build project
24+
run: dotnet build --configuration Release
25+
26+
- name: Test project
27+
run: dotnet test --configuration Release

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
bin/
2+
obj/
3+
/packages/
4+
riderModule.iml
5+
/_ReSharper.Caches/

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Sticks game
2+
3+
Sticks is a 2-player mathematical strategy game.
4+
It is played with a pile/heap of sticks, where players take turns removing sticks from the pile.
5+
The player to take the last stick from the pile loses.
6+
Each player must take at least one and at most *t* sticks.
7+
A game of Sticks is parametric over the number of initial sticks in the pile and the maximum number of sticks a player
8+
may take on their turn (called `maxTake`).
9+
10+
For demonstration and testing purposes, use a pile of 21 sticks and a `maxTake` rule of 3.
11+
That is, a player can take 1, 2, or 3 sticks on their turn.
12+
13+
## Motivation
14+
15+
The game serves as a good introduction to programming concepts such as interfaces[^IPlayer], inheritance[^Bot],
16+
and polymorphism *(see use of concrete player types as `IPlayer` in [SticksGame](Sticks/SticksGame.cs))*.
17+
Students will use their knowledge of class structure to implement their own custom types with fields, constants,
18+
properties, constructors, and methods.
19+
This also offers reinforcement material for loops, conditional statements, and user interaction via the `Console` class.
20+
21+
The following topics are likely to be new material for students:
22+
23+
- Top-down design
24+
- Creating non-static classes
25+
- Interfaces
26+
- Abstract classes, methods, and the `override` keyword
27+
- Property usage
28+
- Domain boundaries, e.g. validating a player's move in the `SticksGame` class instead of each `IPlayer` implementation
29+
- Polymorphism
30+
31+
[^IPlayer]: Sticks/Players/IPlayer.cs
32+
33+
[^Bot]: Sticks/Players/Bot.cs
34+
35+
## Teaching
36+
37+
1. Before coding the program, explain and demonstrate the game with sticks.
38+
2. Explain top-down vs bottom-up design.
39+
3. Have students create `SticksRunner` as the program entrypoint, and finish the body of `SticksRunner.Main` without
40+
implementing its dependencies.
41+
4. Use the IDE code generation tools (found under "quick fix" or a context menu) to generate the classes and methods
42+
used inside `SticksRunner.Main`.
43+
- When implementing the static method `SticksGame.Prompt`, point out that we know it should be static because
44+
in `Main` it's called on the class, not an instance.
45+
46+
[//]: # (TODO: complete teaching notes)
47+
48+
`Sticks/Architecture.puml` is a PlantUML diagram source describes the architecture of the project.
49+
50+
## Requirements
51+
52+
1. Create the program entrypoint `SticksRunner.Main()`.
53+
1. Interactively ask the player for the rules of their game using `SticksGame.Prompt(string prompt)`.
54+
2. Instantiate a `SticksGame` with those rules using the `SticksGame(int initialSticks, int maxTake)` constructor.
55+
3. Run the game using the `Run()` method on the `SticksGame` instance.
56+
2. Create fields in `SticksGame`:
57+
1. The class must have a public constant `MinTake` that equals `1`.
58+
2. Use the constructor to initialize private fields `readonly int _maxTake` and `int _sticks`.
59+
60+
[//]: # (TODO: complete requirements)
61+
62+
[//]: # (TODO: extract player instantiation into SticksRunner)
63+
64+
[//]: # (TODO: implement runtime selecion of players)

Sticks/Architecture.puml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@startuml
2+
'https://plantuml.com/class-diagram
3+
4+
package "Sticks" {
5+
6+
abstract class SticksRunner <<static>> {
7+
+ void Main()
8+
}
9+
' note left: Is responsible for getting MaxTake\nand the initial stick count from the player.
10+
11+
class SticksGame {
12+
{static} + int MinTake
13+
+ int MaxTake
14+
- int _sticks
15+
- IPlayer[] _players
16+
+ {static} int Prompt(string)
17+
+ void Play()
18+
}
19+
20+
package "Sticks.Players" {
21+
interface IPlayer <<interface>> {
22+
string Name
23+
int Take()
24+
}
25+
26+
class Human
27+
abstract class Bot {
28+
-string _id
29+
}
30+
31+
class RandomBot
32+
class SmartBot
33+
class BadActor
34+
}
35+
36+
SticksRunner *- SticksGame : Creates and runs >
37+
SticksRunner -> SticksGame : ""
38+
SticksGame *-- "2" IPlayer
39+
IPlayer <|.- Human
40+
IPlayer <|.- Bot
41+
Bot <|-- RandomBot
42+
Bot <|-- SmartBot
43+
Bot <|-- BadActor
44+
45+
}
46+
@enduml

Sticks/Players/BadActor.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Sticks.Players;
2+
3+
/// <summary>
4+
/// A bot that tries to cheat the game.
5+
/// </summary>
6+
public class BadActor : Bot
7+
{
8+
public override int Take(int sticksLeft, int maxTake) => sticksLeft - 1;
9+
}

Sticks/Players/Bot.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Sticks.Players;
2+
3+
/// <summary>
4+
/// Contains common implementation of bot players, i.e. their unique ID.
5+
/// </summary>
6+
public abstract class Bot : IPlayer
7+
{
8+
/// <summary>
9+
/// The ID of the bot, to distinguish multiple instances of the same bot in a game.
10+
/// </summary>
11+
private static int _id;
12+
13+
public string Name { get; }
14+
15+
protected Bot()
16+
{
17+
Name = $"{GetType().Name} {++_id}";
18+
}
19+
20+
public abstract int Take(int sticksLeft, int maxTake);
21+
}

Sticks/Players/Human.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Sticks.Players;
2+
3+
public class Human : IPlayer
4+
{
5+
public Human(string name)
6+
{
7+
Name = name;
8+
}
9+
public string Name { get; }
10+
public int Take(int sticksLeft, int maxTake)
11+
{
12+
return SticksGame.Prompt($"[{Name}] Take {SticksGame.MinTake}-{maxTake} sticks: ");
13+
}
14+
}

Sticks/Players/IPlayer.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Sticks.Players;
2+
3+
public interface IPlayer
4+
{
5+
string Name { get; }
6+
7+
/// <summary>
8+
/// Takes the player's turn, returning the number of sticks taken.
9+
/// </summary>
10+
/// <param name="sticksLeft">The number of sticks remaining in the pile.</param>
11+
/// <param name="maxTake">The maximum number of sticks a player can take on their turn.</param>
12+
/// <returns>The number of sticks taken by the player on their turn.</returns>
13+
int Take(int sticksLeft, int maxTake);
14+
}

Sticks/Players/RandomBot.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace Sticks.Players;
4+
5+
/// <summary>
6+
/// A bot that makes random moves.
7+
/// </summary>
8+
public class RandomBot : Bot
9+
{
10+
private readonly Random _random = new Random();
11+
12+
public override int Take(int sticksLeft, int maxTake) => _random.Next(SticksGame.MinTake, maxTake + 1);
13+
}

Sticks/Players/SmartBot.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* INSTRUCTOR NOTE:
3+
* Students should implement this class on their own.
4+
*/
5+
6+
namespace Sticks.Players;
7+
8+
public class SmartBot : Bot
9+
{
10+
public override int Take(int sticksLeft, int maxTake) => (sticksLeft - 1) % (maxTake + 1);
11+
}

Sticks/Sticks.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net7.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>

Sticks/SticksGame.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using Sticks.Players;
3+
4+
namespace Sticks;
5+
6+
public class SticksGame
7+
{
8+
public const int MinTake = 1;
9+
private readonly int _maxTake;
10+
private int _sticks;
11+
private readonly IPlayer[] _players;
12+
private int _currentPlayerIdx;
13+
14+
private IPlayer CurrentPlayer => _players[_currentPlayerIdx];
15+
16+
// NOTE: This property changes the value of _currentPlayer
17+
private IPlayer SwitchPlayer =>
18+
_players[_currentPlayerIdx = (_currentPlayerIdx + 1) % _players.Length];
19+
20+
public SticksGame(int sticks, int maxTake)
21+
{
22+
this._sticks = sticks;
23+
this._maxTake = maxTake;
24+
this._players = new IPlayer[]
25+
{
26+
new Human("Player 1"),
27+
new Human("Player 2"),
28+
};
29+
}
30+
31+
public SticksGame(int sticks, int maxTake, IPlayer[] players)
32+
{
33+
this._sticks = sticks;
34+
this._maxTake = maxTake;
35+
this._players = players;
36+
}
37+
38+
public static int Prompt(string prompt)
39+
{
40+
Console.Write(prompt);
41+
int result;
42+
while (!int.TryParse(Console.ReadLine()!, out result) || result <= 0)
43+
{
44+
Console.Write("Invalid input. Please enter a positive integer: ");
45+
}
46+
47+
return result;
48+
}
49+
50+
public void Play()
51+
{
52+
IPlayer player = CurrentPlayer;
53+
while (_sticks > 0)
54+
{
55+
Console.WriteLine($"{player.Name}'s turn. Sticks left: {_sticks}");
56+
57+
int take = ValidateTurn(player, player.Take(_sticks, _maxTake));
58+
59+
_sticks -= take;
60+
player = SwitchPlayer;
61+
}
62+
63+
// The player that did not take the last stick is the winner
64+
IPlayer winner = player;
65+
Console.WriteLine($"{winner.Name} wins!");
66+
67+
Console.WriteLine("Press any key to exit.");
68+
Console.ReadKey();
69+
}
70+
71+
private int ValidateTurn(IPlayer player, int take)
72+
{
73+
if (take > _maxTake)
74+
{
75+
int min = Math.Min(_maxTake, _sticks);
76+
Console.WriteLine($"!! {player} tried to take {take} sticks, which is more than {_maxTake}. " +
77+
$"Limiting their move to {min} sticks.");
78+
return min;
79+
}
80+
81+
if (take < MinTake)
82+
{
83+
Console.WriteLine($"!! {player} tried to take {take} sticks, which is less than {MinTake}. " +
84+
$"Limiting their move to {MinTake} sticks.");
85+
return MinTake;
86+
}
87+
88+
if (take > _sticks)
89+
{
90+
Console.WriteLine($"!! {player} tried to take {take} sticks, which is more than " +
91+
$"the sticks remaining in the pile ({_sticks}). " +
92+
$"Limiting their move to {_sticks}");
93+
return _sticks;
94+
}
95+
96+
return take;
97+
}
98+
}

Sticks/SticksRunner.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
3+
namespace Sticks;
4+
5+
public static class SticksRunner
6+
{
7+
public static void Main()
8+
{
9+
Console.WriteLine("Welcome to the game of Sticks!");
10+
11+
int sticks = SticksGame.Prompt("How many sticks should the game start with? ");
12+
int maxTake = SticksGame.Prompt("What is that maximum number of sticks a player can take? ");
13+
14+
Console.WriteLine();
15+
Console.WriteLine($"There are {sticks} sticks in the pile.");
16+
Console.WriteLine($"On each turn, you can take up to {maxTake} sticks.");
17+
Console.WriteLine("The player who takes the last stick loses.");
18+
Console.WriteLine();
19+
20+
SticksGame game = new SticksGame(sticks, maxTake);
21+
game.Play();
22+
}
23+
}

sticks.sln

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sticks", "Sticks\Sticks.csproj", "{C82B0D9F-D1D2-445D-8FDC-5F1CE5287354}"
4+
EndProject
5+
Global
6+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7+
Debug|Any CPU = Debug|Any CPU
8+
Release|Any CPU = Release|Any CPU
9+
EndGlobalSection
10+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
11+
{C82B0D9F-D1D2-445D-8FDC-5F1CE5287354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12+
{C82B0D9F-D1D2-445D-8FDC-5F1CE5287354}.Debug|Any CPU.Build.0 = Debug|Any CPU
13+
{C82B0D9F-D1D2-445D-8FDC-5F1CE5287354}.Release|Any CPU.ActiveCfg = Release|Any CPU
14+
{C82B0D9F-D1D2-445D-8FDC-5F1CE5287354}.Release|Any CPU.Build.0 = Release|Any CPU
15+
EndGlobalSection
16+
EndGlobal

0 commit comments

Comments
 (0)