UnoAdventure
Instructions
This practical support work serves as a mock exam to prepare you for the final test. It is designed to be completed in 3 hours, but don't worry if you don't finish it completely - it is intentionally a bit longer than the real exam.
The objective is to allow you to review and consolidate your C# programming skills. Don't hesitate to ask questions on Discord if you encounter difficulties.
This is a support lab assignment, serving as a mock exam, offered to you to prepare for the exam. This lab is supposed to be doable in 3 hours, however, no stress if you haven't finished it, it is slightly longer than the exam.
Good luck with your revision and don't hesitate to ask questions as needed, whether on Discord.
Architecture
At the end, your git repository must follow this architecture:
.
├── .gitignore
├── README
├── UnoAdventure
│ ├── Cards
│ │ ├── BasicCard.cs
│ │ ├── Card.cs
│ │ ├── JokerCard.cs
│ │ └── SpecialCard.cs
│ ├── Enums.cs
│ ├── GameManager.cs
│ ├── Player.cs
│ ├── Program.cs
│ ├── UnoAdventure.csproj
│ └── UnoException.cs
└── UnoAdventure.sln
Tree structure verification: You can verify the current tree structure of your repository with the following command:
tree . -I 'obj|bin|.idea|.git' -a
Important requirements before submission:
- Obviously replace firstname.lastname with your login.
- The README file is mandatory.
- The .gitignore file is mandatory.
- Remove all personal tests from your code, except those in the Tests folder.
- The given prototypes must be strictly respected.
- The code MUST compile! Otherwise you won't get a grade.
Project creation example:
dotnet new sln --name UnoAdventure
dotnet new console -n UnoAdventure -f net7.0 -lang 'C#'
dotnet sln add UnoAdventure/UnoAdventure.csproj
Fundamental Concepts
File: UnoException.cs
UnoException
This is an exception that we will use throughout the lab.
Copy it into UnoException.cs
public class UnoException : Exception { }
File: Enums.cs
ColorEnum
This is an enumeration that represents the four colors of Uno cards.
You must define the following colors, associating them with the value in parentheses.
ColorGreen(0)ColorRed(1)ColorBlue(2)ColorYellow(3)
File: Card/Card.cs
Fields
Here, we will define a Card class that represents a Uno card. We will define the different types of cards right after.
The Card class must have the following attributes:
- A private
Valueattribute of typestringthat represents the value of the card (example: "7", "Reverse", etc...).
Constructor
The Card class must have a constructor that initializes the Value attribute from the cardValue passed as parameter.
public Card(string cardValue) { }
Wait for the next function if you want to test the constructor properly.
GetCardValue
The Card class must have a GetCardValue method that returns the Value attribute of the card.
public string GetCardValue() { }
Card card = new Card("7");
Console.WriteLine(card.GetCardValue()); // Displays "7"
Card card2 = new Card("Reverse");
Console.WriteLine(card2.GetCardValue()); // Displays "Reverse"
File: Card/BasicCard.cs
Fields
Here, we will define a BasicCard class that represents a Uno card with a numeric value (example: 6 red).
The BasicCard class must inherit from Card.
The BasicCard class must have the following attributes:
- A public
Colorattribute of typeColorEnumthat represents the color of the card.
Constructor
The BasicCard class must have a constructor that initializes the Value and Color attributes from the cardValue and cardColor passed as parameters.
Note that the value of a basic card cannot exceed a size of 1 and contain a value other than a digit. Otherwise, throw a UnoException.
public BasicCard(string cardValue, ColorEnum cardColor) { }
BasicCard card = new BasicCard("7", ColorEnum.ColorRed);
Console.WriteLine(card.GetCardValue()); // Displays "7"
Console.WriteLine(card.Color); // Displays "ColorRed"
BasicCard card2 = new BasicCard("Reverse", ColorEnum.ColorRed); // Throws UnoException
File: Card/SpecialCard.cs
Fields
Here, we will define a SpecialCard class that represents a special Uno card (example: blue "Reverse").
The SpecialCard class must inherit from Card.
The SpecialCard class must have the following attributes:
- A public
Colorattribute of typeColorEnumthat represents the color of the card.
Constructor
The SpecialCard class must have a constructor that initializes the Value and Color attributes from the cardValue and cardColor passed as parameters.
Note that the value of a special card can only be "Skip", "PickTwo" or "Reverse".
public SpecialCard(string cardValue, ColorEnum cardColor) { }
SpecialCard card = new SpecialCard("Reverse", ColorEnum.ColorBlue);
Console.WriteLine(card.GetCardValue()); // Displays "Reverse"
Console.WriteLine(card.Color); // Displays "ColorBlue"
SpecialCard card2 = new SpecialCard("2", ColorEnum.ColorGreen); // Throws UnoException
File: Card/JokerCard.cs
Fields
Here, we will define a JokerCard class that represents a special Uno card (example: "Joker").
The JokerCard class must inherit from Card.
The JokerCard class must have the following attributes:
- A private
_penaltyattribute of typeushortthat represents the penalty of the card. (example: +4)
Constructor
The JokerCard class must have a constructor that initializes the Value and _penalty attributes from the penalty passed as parameter.
The value of a Joker card is "Joker{penalty}".
The penalty must be a multiple of 2 and must not be greater than (strict) 8.
public JokerCard(ushort penalty) { }
JokerCard card = new JokerCard(4);
Console.WriteLine(card.GetCardValue()); // Displays "Joker4"
JokerCard card2 = new JokerCard(0);
Console.WriteLine(card2.GetCardValue()); // Displays "Joker0"
JokerCard card3 = new JokerCard(7); // Throws UnoException
File: Player.cs
Properties
Here, we will define a Player class that represents a Uno player.
The Player class must have the following properties:
- A public
Nameproperty of typestringthat represents the player's name (with only a getter). - A public
Handproperty of typeCard[]that represents the player's hand (with a getter and setter). - A public
NbCardsproperty of typeushortwith only a getter that must return the number of cards the player has in their hand.
You must change the default getter of NbCards so it can return the number of cards the player has.
Constructor
The Player class must have a constructor that initializes the Name from the name passed as parameter.
The player's hand must be empty at creation (array of size 0).
public Player(string name) { }
Player player = new Player("Alice");
Console.WriteLine(player.Name); // Displays "Alice"
Console.WriteLine(player.NbCards); // Displays 0
GetNumberOfCardsByColor
The Player class must have a GetNumberOfCardsByColor method that returns a dictionary containing the number of cards of each color the player has.
The dictionary keys are the card colors and the associated values are the number of cards of that color.
public Dictionary<ColorEnum, ushort> GetNumberOfCardsByColor() { }
Player player = new Player("Alice");
player.Hand = new Card[] { new BasicCard("7", ColorEnum.ColorRed), new SpecialCard("Reverse", ColorEnum.ColorBlue) };
Dictionary<ColorEnum, ushort> cardsByColor = player.GetNumberOfCardsByColor();
Console.WriteLine(cardsByColor[ColorEnum.ColorRed]); // Displays 1
Console.WriteLine(cardsByColor[ColorEnum.ColorBlue]); // Displays 1
Console.WriteLine(cardsByColor[ColorEnum.ColorGreen]); // Displays 0
Console.WriteLine(cardsByColor[ColorEnum.ColorYellow]); // Displays 0
GetBestColor
The Player class must have a GetBestColor method that returns the most present color in the player's hand.
If the player has the same number of cards of each color, the method must return the color with the smallest associated value (nothing to do with card values). Remember, we associated values with our colors: 0 for green, 1 for red, 2 for blue and 3 for yellow.
public ColorEnum GetBestColor() { }
Player player = new Player("Alice");
player.Hand = new Card[] { new BasicCard("7", ColorEnum.ColorRed), new SpecialCard("Reverse", ColorEnum.ColorBlue) };
Console.WriteLine(player.GetBestColor()); // Displays ColorRed
Player player2 = new Player("Bob");
player2.Hand = new Card[] { new SpecialCard("PickTwo", ColorEnum.ColorRed), new BasicCard("7", ColorEnum.ColorBlue), new BasicCard("8", ColorEnum.ColorBlue) };
Console.WriteLine(player2.GetBestColor()); // Displays ColorBlue
GetCardsByColor
The Player class must have a GetCardsByColor method that returns an array of card lists. Each card list represents cards of one color.
The lists must be ordered by their value (ColorEnum). The array must be of size 5 because there are 4 colors and one list for joker cards.
public List<Card>[] GetCardsByColor() { }
Player player = new Player("Alice");
player.Hand = new Card[] { new BasicCard("7", ColorEnum.ColorRed), new BasicCard("2", ColorEnum.ColorRed), new SpecialCard("Reverse", ColorEnum.ColorBlue), new JokerCard(4) };
List<Card>[] cardsByColor = player.GetCardsByColor();
Console.WriteLine(cardsByColor[(int)ColorEnum.ColorRed].Count); // Displays 2
Console.WriteLine(cardsByColor[(int)ColorEnum.ColorBlue].Count); // Displays 1
Console.WriteLine(cardsByColor[(int)ColorEnum.ColorGreen].Count); // Displays 0
Console.WriteLine(cardsByColor[(int)ColorEnum.ColorYellow].Count); // Displays 0
Console.WriteLine(cardsByColor[4].Count); // Displays 1
Selection Sort
Here we will implement a selection sort algorithm to sort cards of a given color in descending order.
The Player class must have a GetMaxIndexCard method that returns the index of the card with maximum value in the cards list starting from the start index.
The Player class must also have a SortCardsWithSameColor method that sorts the cards in the cards list in descending order by value.
Because card values are string (and they can be digits or words), you must use the String.Compare method to compare card values.
The String.Compare method returns a negative integer if the object is less than the object passed as parameter, 0 if both objects are equal and a positive integer if the object is greater than the object passed as parameter.
We will choose StringComparison.Ordinal to compare card values.
For example:
String.Compare("7", "Reverse", StringComparison.Ordinal); // Returns a negative integer because "7" is less than "Reverse"
String.Compare("7", "7", StringComparison.Ordinal); // Returns 0 because "7" equals "7"
String.Compare("Reverse", "7", StringComparison.Ordinal); // Returns a positive integer because "Reverse" is greater than "7"
public int GetMaxIndexCard(List<Card> cards, int start) { }
Player player = new Player("Alice");
List<Card> cards = new List<Card> { new BasicCard("7", ColorEnum.ColorRed), new SpecialCard("Reverse", ColorEnum.ColorRed), new BasicCard("2", ColorEnum.ColorRed) };
Console.WriteLine(player.GetMaxIndexCard(cards, 0)); // Displays 1
Console.WriteLine(player.GetMaxIndexCard(cards, 2)); // Displays 2
public void SortCardsWithSameColor(List<Card> cards) { }
Player player = new Player("Alice");
List<Card> cards = new List<Card> { new BasicCard("7", ColorEnum.ColorRed), new SpecialCard("Reverse", ColorEnum.ColorRed), new BasicCard("2", ColorEnum.ColorRed) };
player.SortCardsWithSameColor(cards);
Console.WriteLine(cards[0].GetCardValue()); // Displays "Reverse"
Console.WriteLine(cards[1].GetCardValue()); // Displays "7"
Console.WriteLine(cards[2].GetCardValue()); // Displays "2"
SortHand
The Player class must have a SortHand method that sorts the player's hand by color and by descending value.
You must use the GetCardsByColor and SortCardsWithSameColor methods to sort cards by color and value.
You must then rebuild the player's hand by concatenating the lists to create a new array.
public void SortHand() { }
Player player = new Player("Alice");
player.Hand = new Card[] { new BasicCard("7", ColorEnum.ColorRed), new SpecialCard("Reverse", ColorEnum.ColorBlue), new BasicCard("2", ColorEnum.ColorRed) };
player.SortHand();
Console.WriteLine(player.Hand[0].GetCardValue()); // Displays "7"
Console.WriteLine(player.Hand[1].GetCardValue()); // Displays "2"
Console.WriteLine(player.Hand[2].GetCardValue()); // Displays "Reverse"
File: GameManager.cs
Properties
Here, we will define a GameManager class that represents a Uno game.
The GameManager class must have the following properties:
- A public
Playersproperty of typeQueue<Player>that represents the game players. - A public
Deckproperty of typeList<Card>that represents the game card deck. - A public
DiscardPileproperty of typeStack<Card>that represents the game discard pile.
Each property must have a public getter and private setter, except for Deck which must have a public getter and public setter.
Constructor
The GameManager class must have a constructor that initializes the Players, Deck and DiscardPile properties.
Players must be added to the queue in the order of their creation.
The card deck must be initialized empty.
The discard pile must be empty at creation.
public GameManager() { }
GameManager game = new GameManager();
Console.WriteLine(game.Players.Count); // Displays 0
Console.WriteLine(game.Deck.Count); // Displays 0
Console.WriteLine(game.DiscardPile.Count); // Displays 0
AddPlayer
The GameManager class must have an AddPlayer method that adds a player to the game.
public void AddPlayer(Player player) { }
GameManager game = new GameManager();
Player player = new Player("Alice");
Player player2 = new Player("Bob");
game.AddPlayer(player);
game.AddPlayer(player2);
Console.WriteLine(game.Players.Count); // Displays 2
Console.WriteLine(game.Players.Peek().Name); // Displays "Alice"
game.Players.Dequeue();
Console.WriteLine(game.Players.Peek().Name); // Displays "Bob"
AddPlayers
The GameManager class must have an AddPlayers method that adds multiple players to the game.
public void AddPlayers(Player[] players) { }
GameManager game = new GameManager();
Player player = new Player("Alice");
Player player2 = new Player("Bob");
game.AddPlayers(new Player[] { player, player2 });
Console.WriteLine(game.Players.Count); // Displays 2
Console.WriteLine(game.Players.Peek().Name); // Displays "Alice"
game.Players.Dequeue();
Console.WriteLine(game.Players.Peek().Name); // Displays "Bob"
CreateDeck
The GameManager class must have a CreateDeck method that initializes the game card deck.
The card deck must contain two copies of all Uno cards (4 colors from 0 to 9, special cards and jokers with penalty of 0, 4, 8 only).
public void CreateDeck() { }
GameManager game = new GameManager();
game.CreateDeck();
Console.WriteLine(game.Deck.Count); // Displays 110
Advanced Skills
When you use your card's ability, you must also make sure to enqueue the current Player in the players queue.
File: Player.cs
DrawCard
The DrawCard method takes the card deck as parameter. It adds a card to the player by taking the first card from the deck (at index 0).
Don't forget to remove the card from the deck.
public void DrawCard(List<Card> deck) { }
Player player = new Player("Alice");
List<Card> deck = new List<Card> { new BasicCard("6", ColorEnum.ColorRed), new BasicCard("6", ColorEnum.ColorBlue), new BasicCard("6", ColorEnum.ColorGreen) };
player.DrawCard(deck);
// player should have 1 card in his hand -> 6 red
// deck should have 2 cards -> 6 blue, 6 green
UseCard
The UseCard method takes the card to play and the card pile as parameters. It adds the card to the pile then removes the card from the player's hand.
public void UseCard(Card card, Stack<Card> stack) { }
Player player = new Player("Alice");
player.Hand = new Card[] { new BasicCard("6", ColorEnum.ColorRed), new BasicCard("6", ColorEnum.ColorBlue) };
Stack<Card> stack = new Stack<Card>();
player.UseCard(player.Hand[0], stack);
// player should have 1 card in his hand -> 6 blue
// stack should have 1 card -> 6 red
Play
The Play method takes the card pile, card deck, players queue and current color as parameters. It looks at their hand from left to right, if they can play a card, they play it. If they can't play, they draw a card.
They can only play if the card color is identical to the current color or the number is the same as the card on top of the pile or the card is a Joker.
The method must return a boolean indicating whether the player has no more cards in hand.
public bool Play(Stack<Card> pile, List<Card> deck, Queue<Player> players, ColorEnum actualColor) { }
Player player = new Player("Alice");
player.Hand = new Card[] { new BasicCard("6", ColorEnum.ColorBlue) };
Stack<Card> pile = new Stack<Card>();
pile.Push(new BasicCard("6", ColorEnum.ColorRed));
List<Card> deck = new List<Card> { new BasicCard("6", ColorEnum.ColorBlue), new BasicCard("6", ColorEnum.ColorGreen) };
Queue<Player> players = new Queue<Player>( new List<Player> { new Player("Bob"), new Player("Charlie") } );
ColorEnum actualColor = ColorEnum.ColorRed;
bool res = player.Play(pile, deck, players, actualColor);
// players should be -> Alice, Charlie, Bob ->
// Alice should have played the card and the pile should have 2 cards
// Alice should have 0 cards in her hand
// the function should return true
File: Card/SpecialCard.cs
PickTwoCard
The PickTwoCard method takes the current player, players queue and card deck as parameters. It removes the next player from the queue, makes them draw two cards, then puts the current player then the next player back in the queue.
It's important to note that the current player must be put back in the queue before the next player.
public void PickTwoCard(Player actualPlayer, Queue<Player> players, List<Card> deck) { }
Player actualPlayer = new Player("Alice");
actualPlayer.Hand = new Card[] { new BasicCard("7", ColorEnum.ColorRed) };
Player nextPlayer = new Player("Bob");
nextPlayer.Hand = new Card[] { new BasicCard("0", ColorEnum.ColorYellow) };
Queue<Player> players = new Queue<Player>( new List<Player> { nextPlayer, new Player("Charlie") } );
List<Card> deck = new List<Card> { new BasicCard("6", ColorEnum.ColorRed), new BasicCard("6", ColorEnum.ColorBlue), new BasicCard("6", ColorEnum.ColorGreen) };
SpecialCard card = new SpecialCard("PickTwo", ColorEnum.ColorRed);
card.PickTwoCard(actualPlayer, players, deck);
// players should be -> Bob, Alice, Charlie ->
// Bob should have 3 cards in his hand -> 0 yellow, 6 red, 6 blue
SkipCard
The SkipCard method takes the current player and players queue as parameters. It removes the next player from the queue then puts them back in the queue.
It's important to note that the current player must be put back in the queue before the next player.
public void SkipCard(Player actualPlayer, Queue<Player> players) { }
Queue<Player> players = new Queue<Player>( new List<Player> { new Player("Alice"), new Player("Bob"), new Player("Charlie") } );
Player actualPlayer = players.Dequeue();
SpecialCard card = new SpecialCard("Skip", ColorEnum.ColorRed);
card.SkipCard(actualPlayer, players);
// players should be -> Bob, Alice, Charlie ->
Console.WriteLine();
ReverseCard
The ReverseCard method takes the current player and players queue as parameters. It reverses the order of players in the queue.
It's important to note that the current player must be put back in the queue after reversing it.
public void ReverseCard(Player actualPlayer, Queue<Player> players) { }
Queue<Player> players = new Queue<Player>( new List<Player> { new Player("Alice"), new Player("Bob"), new Player("Charlie") } );
Player actualPlayer = players.Dequeue();
SpecialCard card = new SpecialCard("Reverse", ColorEnum.ColorRed);
card.ReverseCard(actualPlayer, players);
// players was -> Charlie, Bob ->
// players is now -> Alice, Bob, Charlie ->
UseCapacity
The UseCapacity method takes the current player, players queue and card deck as parameters. It calls the method corresponding to the card's ability.
public void UseCapacity(Player actualPlayer, Queue<Player> players, List<Card> deck) { }
File: Card/JokerCard.cs
UseCapacity
The JokerCard class is a special card that allows changing the game color. However here we will not handle the color change (which will be done in another function). We will simply make the next player draw cards according to the card's penalty.
The UseCapacity method takes the current player and players queue as parameters. It makes the next player draw cards according to the card's penalty.
The function then puts the current player then the next player back in the queue.
It's important to note that the current player must be put back in the queue before the next player.
public void UseCapacity(Player actualPlayer, Queue<Player> players, List<Card> deck) { }
Queue<Player> players = new Queue<Player>( new List<Player> { new Player("Alice"), new Player("Bob"), new Player("Charlie") } );
List<Card> deck = new List<Card> { new BasicCard("6", ColorEnum.ColorRed), new BasicCard("6", ColorEnum.ColorBlue), new BasicCard("6", ColorEnum.ColorGreen) };
Player actualPlayer = players.Dequeue();
JokerCard jokerCard = new JokerCard(2);
jokerCard.UseCapacity(actualPlayer, players, deck);
// players should be -> Bob, Alice, Charlie ->
// Bob should have 2 cards in his hand -> 6 red, 6 blue
// deck should have 1 card -> 6 green
File: GameManager.cs
ShuffleDeck
The ShuffleDeck method shuffles the player's card deck.
You must create a two-dimensional array of cards with 5 columns and the smallest possible number of rows to contain all the cards in the deck.
You must then go through the deck and place each card in the array, replacing them column by column. (first arr[0][0] then arr[1][0] etc.)
Finally, you must rebuild the deck by going through the array (row by row) and adding each non-null card to the deck.
public void ShuffleDeck() { }
GameManager game = new GameManager();
game.Deck = new List<Card>
{
new BasicCard("1", ColorEnum.ColorRed),
new BasicCard("2", ColorEnum.ColorBlue),
new BasicCard("3", ColorEnum.ColorGreen),
new BasicCard("4", ColorEnum.ColorYellow),
new BasicCard("5", ColorEnum.ColorRed),
new BasicCard("6", ColorEnum.ColorBlue),
new BasicCard("7", ColorEnum.ColorGreen),
new BasicCard("8", ColorEnum.ColorYellow),
new BasicCard("9", ColorEnum.ColorRed)
};
game.ShuffleDeck();
// deck should be:
// done later
// 1 ColorRed
// 3 ColorGreen
// 5 ColorRed
// 7 ColorGreen
// 9 ColorRed
// 2 ColorBlue
// 4 ColorYellow
// 6 ColorBlue
// 8 ColorYellow
CountPoints
The CountPoints method returns a dictionary containing the points of each player.
Each special card is worth 20 points, each normal card is worth its digit and each Joker is worth 50 points.
public Dictionary<Player,int> CountPoints() { }
Player player1 = new Player("Alice");
player1.Hand = new Card[] { new BasicCard("6", ColorEnum.ColorRed), new BasicCard("6", ColorEnum.ColorBlue), new BasicCard("6", ColorEnum.ColorGreen) };
Player player2 = new Player("Bob");
player2.Hand = new Card[] { new SpecialCard("PickTwo", ColorEnum.ColorRed), new SpecialCard("Skip", ColorEnum.ColorBlue), new SpecialCard("Reverse", ColorEnum.ColorGreen) };
GameManager game = new GameManager();
game.AddPlayer(player1);
game.AddPlayer(player2);
Dictionary<Player,int> points = game.CountPoints();
Console.WriteLine(points[player1]); // 18
Console.WriteLine(points[player2]); // 60
DealCards
The DealCards method distributes 7 cards to each player. It must call the DrawCard method each time. The cards must be distributed as in real life. For example if there are 3 players: 1-2-3-1-2-3-1-...
public void DealCards() { }
PlayGame
The PlayGame method is the main game method. It must call the methods created previously to play a complete game.
Here are the different steps to follow:
- Call the
DealCardsmethod - Create the discard pile
DiscardPileand add a yellow 0 (to make it easier for you) - Create a
currentColorvariable that will contain the current color - Enter an infinite loop
- Enter a loop while the card deck is not empty
- Remove the current player from the queue
- Call the current player's
Playmethod - If the player has no more cards, call the
CountPointsmethod and return the result - Change the current color according to the card on top of the pile. If the card is a Joker, then use the
GetBestColormethod of the player who used the card to change the current color.
- Remove the card from the top of the pile and put all the cards from the pile back in the deck
- Shuffle the deck
- Enter a loop while the card deck is not empty
public Dictionary<Player,int> PlayGame() { }