Rock-Paper-Scissors – sprawdzanie wygranej C#
Założenia
Żeby stworzyć najprostszą grę kamień-papier-nożyce, czy Rock-Paper-Scissors, potrzebujemy:
- wyświetlania informacji na ekranie,
- pobierania informacji od osób grających,
- zapisywania tych informacji,
- sprawdzania wyniku.
Pisząc terminalową wersję tej gry w C# potrzebujemy skorzystać kolejno z:
Console.WriteLineConsole.ReadLine- zmiennych, np.
string firstSignistring secondSign - instrukcji warunkowych
if/else if/else - przydatne też mogą być operatory logiczne
&&(i),||(lub).
Pobranie danych
Początek naszej gry może polegać na wyświetleniu informacji dla osób grajacych, pobraniu od nich tekstów reprezentujących wybrane znaki i zapisaniu ich do zmiennych:
Console.WriteLine("Let's play Rock-Paper-Scissors!");
Console.WriteLine("Choose your sign, pleyr 1 (rock/paper/scissors):");
string firstSign = Console.ReadLine()!;
Console.WriteLine("Choose your sign, pleyr 2 (rock/paper/scissors):");
string secondSign = Console.ReadLine()!;
Znak ! na końcu wywołania metody ReadLine z klasy Console daje sygnał kompilatorowi, że choć ReadLine może zwrócić null, czyli nic, to my zakładamy, że tak nie będzie i chcemy, żeby nie wyświetlał nam ostrzeżenia z tym związanego.
Sprawdzanie wygranej
W klasycznej grze Rock-Paper-Scissors na dwie osoby są trzy możliwe wyniki:
- remis,
- wygrana pierwszej osoby,
- wygrana drugiej osoby.
Remis
Remis jest najprostszy do sprawdzenia, ponieważ występuje zawsze, gdy obie osoby podały ten sam znak, bez względu na to, co to był za znak. Możemy wiec zacząc od sprawdzenia czy jest remis i wyświetlenia odpowiedniego komunikatu:
if (firstSign.Equals(secondSign, StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("It's a draw!");
}Tem warunek teoretycznie moglibyśmy zapisać prościej, ot tak:
if (firstSign == secondSign)
{
Console.WriteLine("It's a draw!");
}Wygląda czytelniej, prawda? Jest to jednak wersja, która nie zadziała w sytuacji, kiedy jedna osoba wpisze znak małymi literami, a druga wielkimi. metoda Equals przyjmuje dwie informacje wejściowe – tekst, z którym chcemy porównać wartość naszej zmiennej i informację o tym czy rozmiar liter jest ważny przy porównaniu czy ma zostać zignorowany. W tej konkretnej sytuacji chcemy teksty „rock”, „Rock” i „ROCK” traktować jako równe sobie, dlatego używamy tego pierwszego fragmentu kodu.
Wygrana pierwszej osoby
Jeżeli jednak nie ma remisu, to potrzebujemy sprawdzić czy wygrywa jedna z osób grających. Zanim jednak pokażę jak to zrobić, krótkie przypomnienie zasad (symbol „>” używam jako pokazanie, że znak po lewej wygrywa z znakiem po prawej):
- kamień (rock) > nożyczki (scissors),
- nożyczki (scissors) > papier (paper),
- papier (paper) > kamień (rock).
Oznacza to, że czy tę rundę wygrywa pierwsza osoba jeśli:
- 1. podała rock i 2. podała scissors,
- LUB 1. podała scissors i 2. podała paper,
- LUB 1. podała paper i 2. podała rock.
Można to przedstawić w kodzie na kilka sposobów.
Sposób #1 – wiele prostych instrukcji warunkowych
Niekoniecznie idealny, ale najbardziej podstawowy, wymagający jedynie rozumienia instrukcji warunkowej if oraz powiązanej z nią else if:
if– sprawdź wyrażenie podane w nawiasie okrągłym i jeśli jego wartość totrue, wykonaj pierwsze, co napotkasz zaraz po nawiasie z warunkiem.else if– może wystapić tylko poiflub innej instrukcjielse if– jeżeli wyrażenia przy poprzedzających instrukcjachif/else ifmiały wartośćfalse, to sprawdź czy wyrażenie podane w nawiasie okrągłym ma wartośćtrue – jeśli tak, to wykonaj pierwsze, co napotkasz zaraz po nawiasie z warunkiem.
Można zacząć od przyjęcia założenia, że każdy znak, który może podać osoba pierwsza będziemy rozpatrywać jako osobny warunek. Rozwijając więc nasz kod z remisem mielibyśmy taki zalążek:
if (firstSign.Equals(secondSign, StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("It's a draw!");
}
else if (firstSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase))
{
// jakieś działania, by upewnić się, że osoba pierwsza wygrywa...
}
else if (firstSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase))
{
// jakieś działania, by upewnić się, że osoba pierwsza wygrywa...
}
else if (firstSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase))
{
// jakieś działania, by upewnić się, że osoba pierwsza wygrywa...
}Co powinno się znaleźć w tych trzech miejscach, gdzie aktualnie jest komentarz? Sprawdzenie drugiej części ważnej do określenia kto wygrał – czy druga osoba podała znak, który przegrywa ze znakiem podanym przez osobę pierwszą. Mogłoby to wyglądać tak:
if (firstSign.Equals(secondSign, StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("It's a draw!");
}
else if (firstSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase))
{
if (secondSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("First player won!");
}
}
else if (firstSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase))
{
if (secondSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("First player won!");
}
}
else if (firstSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase))
{
if (secondSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("First player won!");
}
}Jeżeli połączymy to z kodem do pobierania znaków od osób grających i wrzucimy np. do Visual Studio Code i skompilujemy, to i podamy odpowiednie znaki po uruchomieniu gry, to zarówno obsługa remisu, jak i wygranej pierwszej osoby powinno działać poprawnie. Da się to jednak jeszcze trochę skrócić.
Sposób #2 – wykorzystanie operatora logicznego && (i)
Chcemy żeby tekst „First player won!” wyświetlał się we wszystkich przypadkach przedstawionych w kolejnych instrukcjach else if. Zobaczmy, czy da się coś tutaj uprościć.
Pierwszym krokiem w stronę uproszczenia będzie zwrócenie uwagi na to, że jeśli druga osoba podała inny znak niż ten przegrywający z osobą pierwszą, to nie robimy aktualnie nic. Tekst o wygranej pierwszej osoby wyświetlamy tylko, kiedy zarówno warunek z else if jak i zagnieżdżonego w nim if będą jednocześnie spełnione. W języku naturalnym możemy powiedzieć, że chcemy wyświetlić ten tekst, kiedy prawdziwy jest zewnętrzny i wewnętrzny warunek. W językach programowania również istnieje coś, co pozwala nam łączyć w ten sposób kilka wyrażeń i dzięki temu umieścić je razem. W C# jest to operator logiczny i, który zapisujemy jako &&.
Zmodyfikujmy więc nasz kod, by z niego skorzystać:
if (firstSign.Equals(secondSign, StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("It's a draw!");
}
else if (firstSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("First player won!");
}
else if (firstSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("First player won!");
}
else if (firstSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("First player won!");
}Teraz zamiast zagnieżdżonych instrukcji warunkowych mamy ich mniej, ale za to wyrażenia w nich zawarte są dłuższe, więc można dyskutować czy to jest lepsze rozwiązanie. Co istotne, to i poprzednie rozwiązania nie różnią się na ten moment, ale jakbyśmy chcieli dodawać już obsługę wygranej drugiej osoby, to w poprzednim rozwiązaniu byłoby to bardziej skomplikowane. Ale do tego przejdziemy za chwilę.
Sposób #3 – wykorzystanie dodatkowo operatora logicznego || (lub)
Jeżeli bym postanowił zmienić to, co ma się dziać kiedy wygra pierwsza osoba, musiałbym to zmienić w trzech miejscach. Aktualnie nie jest to nic szalonego, ale jeśli by ten kod nam się rozbudował, to każde takie powtórzenie może generować problemy, jeśli zapomnimy zmienić jedno z nich. Co możemy zrobić?
Rozwiązań jest kilka, ale aktualnie chcę się skupić na dodaniu jeszcze jednego operatora logicznego – lub – przedstawianego w C# jako ||.
O ile operator && pozwala łączyć dwa wyrażenia logiczne (takie, których wynikiem jest prawda lub fałsz) tak, że wynikiem jest prawda tylko, jeśli oba te wyrażenia mają wartość true, o tyle operator || przydaje się, gdy wystarczy, by tylko jedno z łączonych wyrażeń miało wartość true, by całe wyrażenie było prawdziwe.
W jaki sposób nam się to przyda? Operator && przydał się do sprawdzenia czy dwie zmienne mają jednocześnie oczekiwane wartości. Z kolei operator || przyda się by połączyć trzy przypadki, kiedy może wygrywać osoba pierwsza, a te przypadki nawzajem się wykluczają – niemożliwe jest, by jednocześnie osoba pierwsza podała „rock” i „paper”, ale oba mogą same z siebie wystąpić.
Użyjemy operatora || do złączenia instrukcji warunkowych z trzech else if do jednego:
if (firstSign.Equals(secondSign, StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("It's a draw!");
}
else if ((firstSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase))
|| (firstSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase))
|| (firstSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase)))
{
Console.WriteLine("First player won!");
}Zrobiło się trochę tłoczno, co nie? Warto zwrócić uwagę, że dla czytelności objąłem nawiasem okrągłym każde z wyrażeń, które wcześniej było wyrażeniem w osobnym else if. Nawiasy w takich sytuacjach mogą pomagać w analizowaniu kodu lub mogą zmieniać kolejność wykonywanych działań, jak w matematyce.
Wygrana drugiej osoby
Namęczyłem się tutaj trochę z tymi różnymi wersjami dla obsługi wygranej pierwszej osoby. Czy będę musiał zrobić analogiczne warunki, tylko z zamienionymi wartościami, dla wygranej drugiej osoby? Choć może się to wydawać intuicyjne, to jest prostsze rozwiązanie. Wystarczy się przyjrzeć się ponownie zasadom i możliwym wynikom. Skoro sprawdziliśmy już remis i wygraną pierwszej osoby, to czy jest w ogóle jeszcze jakaś inna opcja poza wygraną drugiej osoby?
Mamy tylko trzy możliwe scenariusze i teraz potrzebujemy obsłużyć ten trzeci. Może on wystąpić tylko i wyłącznie, jeśli żaden z poprzednio obsłużonych scenariuszy nie wystąpił. Jest w C# słowo kluczowe, które pozwala to banalnie obsłużyć – else.
Nie musimy przy nim podawać żadnych wyrażeń, których wartość by była sprawdzana. Po prostu używa się go jako stwierdzenie „Jeśli żaden z poprzedzających mnie warunków nie został spełniony, to wykonaj pierwsze, co po mnie zobaczysz.” A w kodzie wygląda to tak:
if (firstSign.Equals(secondSign, StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("It's a draw!");
}
else if ((firstSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase))
|| (firstSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase))
|| (firstSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase)))
{
Console.WriteLine("First player won!");
}
else
{
Console.WriteLine("Second player won!");
}W ten sposób mówimy naszemu programowi, żeby sprawdził remis, a jeśli nie ma remisu, by sprawdził czy wygrywa pierwsza osoba. Jeżeli i to nie jest prawdą, czyli w każdym innym przypadku, wyświetl, że wygrywa osoba druga.
Cały kod
Na podsumowanie zostawiam cały kod – pobieranie danych, zapisywanie ich oraz sprawdzanie kto zwyciężył i informowanie osób grających o tym:
Console.WriteLine("Let's play Rock-Paper-Scissors!");
Console.WriteLine("Choose your sign, pleyr 1 (rock/paper/scissors):");
string firstSign = Console.ReadLine()!;
Console.WriteLine("Choose your sign, pleyr 2 (rock/paper/scissors):");
string secondSign = Console.ReadLine()!;
if (firstSign.Equals(secondSign, StringComparison.CurrentCultureIgnoreCase))
{
Console.WriteLine("It's a draw!");
}
else if ((firstSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase))
|| (firstSign.Equals("scissors", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase))
|| (firstSign.Equals("paper", StringComparison.CurrentCultureIgnoreCase) && secondSign.Equals("rock", StringComparison.CurrentCultureIgnoreCase)))
{
Console.WriteLine("First player won!");
}
else
{
Console.WriteLine("Second player won!");
}Ale czy na pewno wszystko działa tak jak powinno?
Bardzo dobre pytanie. I odpowiedź jest dość prosta – nie. To jednak opiszę w osobnym wpisie. Tymczasem zachęcam do testowania.

