gra w kółko i krzyżyk - wersja webowa

0
<!DOCTYPE html>
<html lang = "pl">

<head>
    <meta charset = "UTF-8">
    <meta name = "viewport" content = "width=device-width, initial-scale = 1.0, shrink-to-fit = no">
    <link rel = "stylesheet" href = "KiK.css">
    <script src = "KiK.js" defer></script>
</head>

<body>
    <div id = "main">
        <div id = "board">
            <table id = "table">
                <tr>
                    <td  class = "free" id = "f1">1</td>
                    <td  class = "free" id = "f2">2</td>
                    <td  class = "free" id = "f3">3</td>
                </tr>
                <tr>
                    <td  class = "free" id = "f4">4</td>
                    <td  class = "free" id = "f5">5</td>
                    <td  class = "free" id = "f6">6</td>
                </tr>
                <tr>
                    <td  class = "free" id = "f7">7</td>
                    <td  class = "free" id = "f8">8</td>
                    <td  class = "free" id = "f9">9</td>
                </tr>
            </table>
        </div>
        <div id = "panel">
            <input type = "button" value = "Start game!" id = "start">
            <input type = "button" value = "Reset game!" id = "reset" disabled = "true">
            <p id = "round"></p>
            <p id = "score"></p>
        </div>
    </div>
</body>

</html>
body 
{
    margin: 0;
    overflow: auto;
    padding: 0;
}

/* --- DIV --- */
#main
{
    height: 100vh;
    margin: auto;
    width: 100vw;
}
#board
{
    background-color: blue;
    float: left;
    height: 100%;
    width: 70%
}
#panel
{
    background-color: lightgray;
    float: left;
    height: 100%;
    width: 30%
}

/* --- table --- */
table
{
    border-collapse: collapse;
    margin: auto;
    margin-top: 10%;
}
td
{
    border: 5px solid darkblue;
    border-collapse: collapse;
    color: lightgray;
    font-size: 75px;
    height: 100px;
    text-align: center;
    transition: all .1s ease-in-out;
    width: 100px;
}
td:hover
{
    background-color: rgb(255, 217, 0);
    color: rgb(255, 217, 0);
}

/* --- panel --- */
#start, #reset
{
    display: block;
    font-size: 25px;
    margin: auto;
    margin-top: 20px;
    width: 90%;
}
p
{
    color: black;
    font-size: 20px;
    text-align: center;
}
let gamestarted = false;
let turn = 1;
let player = "O";

let fields = document.getElementsByClassName("free");
let startbutton = document.getElementById("start");
let resetbutton = document.getElementById("reset");
let round = document.getElementById("round");
let score = document.getElementById("score");

// -------------------------------------------------------

// start new game button
startbutton.addEventListener("click", () => 
{
    gamestarted = true;
    startbutton.disabled = true;
    resetbutton.disabled = false;
    round.textContent = "Round 1, player " + player + " starts!";
})

// reset game button
resetbutton.addEventListener("click", () => 
{
    gamestarted = false;
    startbutton.disabled = false;
    resetbutton.disabled = true;
    clearBoard();
})

// -------------------------------------------------------

// click on fieds 1-9 and do stuff
for (let n = 0 ; n < 9 ; n++) 
{
    fields[n].addEventListener("click", function() 
    {
        let value = fields[n].textContent;
        let id = this.id;
        if (gamestarted)
        {
            fillBoard(value, id);
            checkScore();
        }
        if (gamestarted)
        {
            newTurn();
            displayScore(); 
        }
    })
}

// -------------------------------------------------------

// fill with X or O
function fillBoard(value, id)
{
    let numbers = /[1-9]/;
    if (numbers.test(value))
    {
        document.getElementById(id).textContent = player;
    }
}

// check if someone has won
function checkScore()
{
    if (turn > 8)
    {
        gamestarted = false;
        round.textContent = "";
        score.textContent = "End of game! No winner!";
    }

    if 
    (
        ((fields[0].textContent == fields[1].textContent) && (fields[1].textContent == fields[2].textContent)) ||
        ((fields[3].textContent == fields[4].textContent) && (fields[4].textContent == fields[5].textContent)) ||
        ((fields[6].textContent == fields[7].textContent) && (fields[7].textContent == fields[8].textContent)) ||
        ((fields[0].textContent == fields[3].textContent) && (fields[3].textContent == fields[6].textContent)) ||
        ((fields[1].textContent == fields[4].textContent) && (fields[4].textContent == fields[7].textContent)) ||
        ((fields[2].textContent == fields[5].textContent) && (fields[5].textContent == fields[8].textContent)) ||
        ((fields[0].textContent == fields[4].textContent) && (fields[4].textContent == fields[8].textContent)) ||
        ((fields[2].textContent == fields[4].textContent) && (fields[4].textContent == fields[6].textContent))
    )
    {
        gamestarted = false;
        round.textContent = "";
        score.textContent = "End of game! Player " + player + " has won!";
    }
}

// set new round and change player
function newTurn()
{
    turn++;
    turn % 2 == 0 ? player = "X" : player = "O";
}

// display score and round number
function displayScore()
{
    round.textContent = "Round " + turn + ", player's " + player + " turn";
}

// reset variables and clear board
function clearBoard()
{
    turn = 1;
    player = "O";
    for (let n = 0 ; n < 9 ; n++) 
    {
        fields[n].textContent = n+1;
    }
    round.textContent = "";
    score.textContent = "";
}
2
#panel
{

Stylowanie po id to słabe podejście. Lepiej stylować po klasach (wtedy możesz bez obaw zrobić kilka takich samych elementów, jak będziesz chciał zrobić np. kilka gier na jednym ekranie.

let value = fields[n].textContent;
// ...
let numbers = /[1-9]/;
if (numbers.test(value))

Tutaj traktujesz drzewko DOM jako bazę danych, z której wyciągasz informacje na temat stanu planszy. Słabe to jest, bo uzależniasz logikę gry od tego, co masz w DOM. Bardziej eleganckie podejście wymagałoby, żebyś trzymał informacje o planszy w osobnej zmiennej (jeśli masz 9 pól, to mogłaby to być np. 9 elementowa tablica w JavaScript). Wtedy logika gry byłaby osobno od interfejsu (i łatwiej można byłoby wymienić interfejs albo logikę bez ruszania obu). A u ciebie to wszystko pomieszane jest.

Przez to również masz strasznie nieczytelne kody, bo to, co powinno być czystym JavaScriptem, to wyciągasz dopiero z DOMu:

((fields[0].textContent == fields[1].textContent) && (fields[1].textContent == fields[2].textContent)) ||
        ((fields[3].textContent == fields[4].textContent) && (fields[4].textContent == fields[5].textContent)) ||

Dobrym ćwiczeniem jest zrobić grę w ten sposób, żeby zrobić tę samą grę w dwóch wersjach graficznych/interfejsu - np. tutaj mógłbyś zrobić grę w DOM oraz drugą wersję opartą o Canvas (i żeby można było się przełączać). Wtedy łatwiej jest ogarnąć i utrzymać tę granicę między logiką gry a interfejsem.

function checkScore()
{

No i kwestia stylu. Jest to subiektywne, ale większość JavaScriptowców napisze klamerkę w tej samej linijce:

function checkScore() {

i w paru innych miejscach też twój styl jest nietypowy.
np. dajesz spacje między atrybutami w HTML:

  <input type = "button" value = "Reset game!" id = "reset" disabled = "true">

To dziko wygląda, mimo, że w JS pisze się zwykle ze spacjami przed i po =, to w HTML zwykle się te spacje omija.

 let gamestarted = false;

tutaj bardziej elegancko by wyglądało gameStarted niż gamestarted i w innych zmiennych podobnie.

2

Oprócz tego co zostało wymienione w pierwszym poście to:

  1. pisanie nazw plików w DzIwNy SpOsÓb (KiK.js, KiK.css), zazwyczaj przyjmuje się, że pliki wynikowe z kompilowanym javascriptem/cssem są z małych liter

  2. większość tych zmiennych można spokojnie zamienić na stałe const.

const fields = document.getElementsByClassName("free");
const startButton = document.getElementById("start");
const resetButton = document.getElementById("reset");
const round = document.getElementById("round");
const score = document.getElementById("score");

Ma to o tyle taką zaletę, że od razu na samym początku widać bez jakieś analizy skryptu, że wartość tego się nie zmieni.
Co prawda może się zmienić właściwość obiektu startButton.id = "new-id", ale nie sam obiekt startButton = document.getElementById('other-button'), ponieważ javascript wyrzuci nam błąd.

  1. Zmienne globalne... Skrypt ma jedyne 117 linijek, a już powstają dziwne, rzeczy w stylu
if (gamestarted)
{
  fillBoard(value, id);
  checkScore(); // <-- funkcja checkScore zmienia globalną zmienną gamestarted
}

if (gamestarted) // <-- przez co ponownie musiałeś powtórzyć ten sam warunek
{
  newTurn(); // <-- dodatkowo istnieje ryzyko, że te funkcje
  displayScore();  // <-- także mogą modyfikować te same zmienne
}
  1. Nie jest to jakiś szczególny błąd w tym wypadku, bo tabela jest mała, ale podpiąłeś 9 razy ten sam event, a wystarczyłby jeden bezpośrednio na tabeli
for (let n = 0 ; n < 9 ; n++) 
{
    fields[n].addEventListener("click", function() {

Ma to sens szczególnie, gdy mamy bardzo dużo takich samych elementów i chcemy jak najbardziej zoptymalizować kod

  1. Z poziomu javascriptu teoretycznie nie wiesz, że tabela ma 9 pól, bo z tego co widzę nigdzie tego nie sprawdzasz. W taki sposób bardzo łatwo wyjść poza zakres tablicy
for (let n = 0 ; n < 9 ; n++) // pętla powinna wyglądać tak for (let n = 0, fieldsLength = fields.length; n < fieldsLength; n++) { 
{
    fields[n].addEventListener("click", function(
  1. Używanie operatora ==, który sprawdza jedynie wartość zamiast ===, który dodatkowo sprawdza także typ
console.log(1 == 1) // true
console.log(1 === 1) // true

console.log(1 == "1") // true
console.log(1 === "1") // false

console.log(0 == false) // true
console.log(0 === false) // false

console.log(1 == true) // true
console.log(1 === true) // false
0

Spodziewałem się, że ktoś zwróci uwagę na zmienne globalne. Tylko czy faktycznie można je pominąć w tym przypadku? Tutaj nie ma funkcji main(), którą mógłbym zapętlić. Z drugiej strony może faktycznie, całą logikę można ułożyć inaczej. Użyte zmienne to typy proste i z tego co widzę i rozumiem, w funkcjach przekazuje je jako wartość, a nie referencje. Stąd zamysł, by funkcje "dobierały" się do zmiennych globalnych bezpośrednio.

Początkowo event przypiąłem do tabelki. Ale efekt był taki, że po kliknięciu na daną komórkę (element td) łapało mi id elementu parent, czyli table. Dlatego zdecydowałem się podpiąć event pod każdy td. No chyba, że istnieje sposób, by dostać się do childa.

1

Spodziewałem się, że ktoś zwróci uwagę na zmienne globalne. Tylko czy faktycznie można je pominąć w tym przypadku? Tutaj nie ma funkcji main(), którą mógłbym zapętlić. Z drugiej strony może faktycznie, całą logikę można ułożyć inaczej. Użyte zmienne to typy proste i z tego co widzę i rozumiem, w funkcjach przekazuje je jako wartość, a nie referencje. Stąd zamysł, by funkcje "dobierały" się do zmiennych globalnych bezpośrednio.

Tutaj zależy od podejścia, bo javascript pozwala na programowanie funkcyjne i obiektowe.

W podejściu funkcyjnym unikałbym modyfikacji zmiennych globalnych i jak najwięcej starałbym się korzystać z argumentów i słówka kluczowego return.
Wszystko po to, żeby unikać sytuacji jak niżej

let testValue = '';

function funcA() {
  testValue = 'function-a';

  funcB();

  if (testValue === 'function-a') { // ten warunek nigdy się nie wykona, bo funkcja "funcB" modyfikuje globalnie zmienną "testValue"
    console.log(`testValue ma wartość ${testValue}`);
  } else {
    console.log('testValue zostało zmienione w czasie trwania funkcji');
  }
}

function funcB() {
  testValue = 'function-b';
}

Obiektowo:

class Game {
  static turn = 1;
  static started = false;

  static newTurn() {
    // ...
  }
}

class Board {
  static fill() {
    // ...
  }

  static clear() {
    // ...
  }
}

class Player {
 constructor() {
    // ...
 }

 move() {
   // ...
 }
}

const playerA = new Player();
const playerB = new Player();

Później na pewno dojdziesz do momentu, gdzie będziesz dzielić kod na kilka plików, dojdą jakieś moduły co powinno pomóc w lepszej organizacji projektu.
Na ten moment raczej nie ma sensu się tym przejmować tylko najlepiej dalej się uczyć składni języka

Początkowo event przypiąłem do tabelki. Ale efekt był taki, że po kliknięciu na daną komórkę (element td) łapało mi id elementu parent, czyli table. Dlatego zdecydowałem się podpiąć event pod każdy td.
No chyba, że istnieje sposób, by dostać się do childa.

Za każdym razem jak podpinasz jakiś event to do callbacka w addEventListener jest przekazywany argument dotyczący eventu.

e.target odnosi się do jakiegokolwiek elementu, który wywołał zdarzenie i w tym przypadku może być elementem td, p, lub span.

Natomiast e.currentTarget odnosi się zawsze do elementu, który ma przypisanie zdarzenie i w tym przykładzie jest to table.

<table>
  <tr>
    <td>
      TD 1
      <p class="akapit">Akapit 1</p>
      <span>Span 1</span>
    </td>
    <td>
      TD 2
      <p class="akapit">Akapit 2</p>
      <span>Span 2</span>
    </td>
    <td>
      TD 3
      <p class="akapit">Akapit 3</p>
      <span>Span 3</span>
    </td>
  </tr>
</table>
const table = document.querySelector('table');

table.addEventListener('click', (e) => { // pod parametrem "e" kryje się nasz event click 
  console.log(e.target.textContent); // e.target to jest element, który wywołał zdarzenie
  
  // oczywiście możemy dokładniej sprawdzić, który element został kliknięty po przez różne atrybuty
  console.log(e.target.classList.contains('akapit')); // tutaj sprawdzamy, czy element ma klasę "akapit"
  console.log(e.target.tagName); // tutaj zostanie wyświetlona nazwa elementu

  console.log(e.currentTarget) // nasza tabelka
})

Pewnie musiałbyś zmienić lekko strukturę gry żeby to działało, ale wspomniałem bardziej o tym w ramach takiej ciekawostki niż faktycznego błędu.
Taka drobna optymalizacja, bo przy dużych tabelach, gdzie byłoby kilkadziesiąt/kilkaset wierszy miałoby to większe znaczenie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1