Jak vytvořit Tic Tac Toe v prohlížeči pomocí HTML CSS a JS

Nuda být sám? Nebo se stydíte zavolat svým přátelům, aby si s vámi zahráli piškvorky? Pojďme udělat piškvorky s HTML, CSS a JavaScript. Než ale začnete, zřeknutí se odpovědnosti:k vytvoření tahů počítače nebyla použita žádná umělá inteligence, takže pro umístění svého tahu pouze vybírá náhodné bloky. Pokud bude tento příspěvek populární, pokusím se tuto hru vylepšit pomocí AI. Takže, bez dalšího loučení, pojďme rovnou do tohoto projektu. Veškerý kód použitý v tomto projektu je dostupný na Codepen

Označení

Jelikož se jedná o projekt zaměřený na javascript, nebudu se na značkování moc zaměřovat, ale pro ty, kteří chtějí podobné uživatelské rozhraní jako já, může následovat.

HTML začalo obvyklou deklarací hlavy s propojením šablony stylů a deklarováním názvu:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Tic-Tac-Toe</title>
    <link rel="stylesheet" href="style.css" />
  </head>
</html>

Poté jsem potřeboval nádobu, do které bych vše na této stránce zabalil a přenesl do středu, což bylo v těle provedeno takto:

<body>
  <div class="container"></div>
  <script src="app.js"></script>
</body>

V tuto chvíli jsem také propojil javascript, abych na to nezapomněl.

Nyní jsem v HTML vytvořil hrací oblast tj. skutečná deska, ale protože 9 bloků uvnitř bloku bude mít podobnou vlastnost, požádám javascript, aby mi je vykreslil později, ale nyní, abych viděl, jak bude deska vypadat, je přidám bez události kliknutí. A také jsem přidal název, aby deska vypadala cool:

<div class="container">
  <h1>Tic-Tac-Toe</h1>
  <div class="play-area">
    <div id="block_0" class="block"></div>
    <div id="block_1" class="block"></div>
    <div id="block_2" class="block"></div>
    <div id="block_3" class="block"></div>
    <div id="block_4" class="block"></div>
    <div id="block_5" class="block"></div>
    <div id="block_6" class="block"></div>
    <div id="block_7" class="block"></div>
    <div id="block_8" class="block"></div>
  </div>
</div>

Použil jsem tato ID, abych dal desce vzhled desky jako tic-tac-toe.

Nyní jsem nechtěl obnovit stránku, abych resetoval tabuli pro nový zápas nebo abych začal. Takže jsem přidal tlačítko s funkcí onClick, kterou implementuji velmi pozdě, abych resetoval desku.

<div class="container">
  <!-- REST OF THE CODE -->
  <h2 id="winner"></h2>
  <button onclick="reset_board()">RESET BOARD</button>
</div>

Zde jsem také přidal h2 s id vítěze, abych později přidal text, kdo je vítěz.

Nyní přejdeme k CSS.

Pro začátek jsem resetoval okraj a odsazení a nastavil velikost pole a výchozí písmo:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: Arial, Helvetica, sans-serif;
}

Nyní, abych celou hru vycentroval doprostřed prohlížeče, použil jsem na kontejner tento styl:

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #eee;
}

Tlačítko resetu bylo několik efektů vznášení, jako je tento:

button {
  outline: none;
  border: 4px solid green;
  padding: 10px 20px;
  font-size: 1rem;
  font-weight: bold;
  background: none;
  transition: all 0.2s ease-in-out;
}

button:hover {
  cursor: pointer;
  background: green;
  color: white;
}

Pak tu byl CSS pro vytvoření původní desky:

.play-area {
  display: grid;
  width: 300px;
  height: 300px;
  grid-template-columns: auto auto auto;
}
.block {
  display: flex;
  width: 100px;
  height: 100px;
  align-items: center;
  justify-content: center;
  font-size: 3rem;
  font-weight: bold;
  border: 3px solid black;
  transition: background 0.2s ease-in-out;
}

.block:hover {
  cursor: pointer;
  background: #0ff30f;
}

Nejprve jsem vytvořil herní plochu tak, aby vypadala jako mřížka, abych mohl umístit 9 bloků rovnoměrně. Nyní jsem každému bloku přidělil třídu blok . Vybral jsem tedy bloky a dal jim kolem dokola ohraničení. Udělal jsem je také tak, aby zobrazovaly symboly uprostřed tím, že jsem je nastavil jednotlivě jako flex a dal těmto flex align-items a zdůvodnit obsah majetek centra. Velikost písma a tloušťka písma byly nastaveny tak, aby byly pohyby výraznější a viditelnější. Přechod pozadí byl nastaven tak, aby bylo možné zobrazit změnu barvy pozadí, pokud je kurzor umístěn nad daným blokem.

Když teď mluvíme o vznášení, nastavil jsem kurzor na ukazatel a pozadí na jasně zelenou, což znamená, že hráč tam může provést tah. Takže abych naznačil, kde hráči nemohou dát tah, rozhodl jsem se označit blok s obsazenou třídou, jakmile bude mít nějaký obsah. Tak jsem přidal toto:

.occupied:hover {
  background: #ff3a3a;
}

V tomto okamžiku deska vypadala dobře, ale chtěl jsem klasický vzhled. Nyní, když je deska položena jedna po druhé, id každého div je takto:

Initial board:
 ______ ___
| 0 | 1 | 2 |
 ______ ___
| 3 | 4 | 5 |
 ______ ___
| 6 | 7 | 8 |
 ______ ___

Takže se musíme odstěhovat:

  • horní ohraničení pro divs s ID 0,1,2
  • levý okraj pro divs s ID 0,3,6
  • spodní ohraničení pro divs s ID 6,7,8
  • pravé ohraničení pro divs s ID 2,5,8

Pak bude naše deska vypadat takto:

Now board:

  0 | 1 | 2
 ______ ___
  3 | 4 | 5
 ______ ___
  6 | 7 | 8

Takže jsem to udělal ve svém CSS:

#block_0,
#block_1,
#block_2 {
  border-top: none;
}

#block_0,
#block_3,
#block_6 {
  border-left: none;
}

#block_6,
#block_7,
#block_8 {
  border-bottom: none;
}

#block_2,
#block_5,
#block_8 {
  border-right: none;
}

Aby aplikace vypadala krásně, je tu ještě pár CSS, ale já je přeskočím a vrhnu se rovnou do masa, tedy javascriptu.

Nyní aplikace vypadá takto:

JavaScript

Pro spuštění javascriptu jsem vytvořil desku v poli v javascriptu:

let play_board = ["", "", "", "", "", "", "", "", ""];

Poté jsem se místo pevného kódování funkcí v každém z prvků v HTML rozhodl vykreslit desku pomocí JS a udělal jsem to tak, že jsem deklaroval funkci render a zavolal ji:

const board_container = document.querySelector(".play-area");

const render_board = () => {
  board_container.innerHTML = "";
  play_board.forEach((e, i) => {
    board_container.innerHTML += `<div id="block_${i}" class="block" onclick="addPlayerMove(${i})">${play_board[i]}</div>`;
    if (e == player || e == computer) {
      document.querySelector(`#block_${i}`).classList.add("occupied");
    }
  });
};

render_board();

Zde jsem nejprve potřeboval odkaz na kontejner kontejneru/hrací plochy. Tak jsem to vybral pomocí document.querySelector() . Poté jsem ve funkci render board nejprve odstranil veškerý vnitřní obsah našeho board_container. A poté jsem pomocí smyčky for-each na prvcích hrací desky přidal div pro každý blok s jejich specifickým id a jejich specifickou funkci přidání pohybu s ID bloku.

V tomto okamžiku jsem také přidal javascript, abych přidal obsazenou třídu do bloků s prvky v ní.

Další věcí bylo deklarovat tuto funkci addPlayerMove, která vzala id/index div, na který jste klikli, umístila tah a požádala počítač, aby provedl svůj pohyb, ale předtím jsem se rozhodl, že počítač by měl vzít kus „X“ a hráč by měl umístit dílek "O". Takže jsem tyto dvě deklaroval jako konstanty a začal jsem programovat addPlayerMove()

const player = "O";
const computer = "X";

const addPlayerMove = e => {
  if (play_board[e] == "") {
    play_board[e] = player;
    render_board();
    addComputerMove();
  }
};

Bylo to tak snadné, jako změnit tento prvek na desce pole založené na javascriptu a požádat desku o vykreslení a poté požádat počítač, aby provedl svůj pohyb. Jediná věc, kterou jsem se potřeboval ujistit, je, že místo je prázdné, abych mohl provést tah.

Nyní musíme deklarovat addComputerMove()

const addComputerMove = () => {
  do {
    selected = Math.floor(Math.random() * 9);
  } while (play_board[selected] != "");
  play_board[selected] = computer;
  render_board();
};

Aby byl tento příspěvek jednoduchý, požádal jsem počítač, aby vybral náhodný blok z 0 až 8, ale ujistěte se, že předtím nebyl proveden žádný přesun.

Gratuluji! nyní můžete hrát hru a umístit tahy. Existují však určité problémy. Pojďme je oslovit jeden po druhém.

První problém je, že počítač chce provést tah i po dokončení desky. Udělejme tedy funkci, která zkontroluje, zda je deska kompletní nebo ne, a vyčleníme pro to boolean:

let boajsrd_full = false;
const check_board_complete = () => {
  let flag = true;
  play_board.forEach(element => {
    if (element != player && element != computer) {
      flag = false;
    }
  });
  board_full = !flag;
};

Zde byla nejprve proměnná board_full nastavena na false. Pak je ve funkci příznak nastavený na true. Poté jsem pomocí smyčky for-each prošel každý prvek. Pokud byl nalezen prvek s "", tj. prázdný, tj. žádný hráč nebo počítač se nehýbal, pak byl příznak nastaven na hodnotu false. Když byla smyčka dokončena, hrací plocha byla plná, pokud byla vlajka pravdivá, jinak nebyla plná. Takže hodnota board_full byla jen hodnota flag.

Protože tato kontrola a vykreslování bude prováděno po každém tahu, pojďme je dát dohromady do funkce nazvané game_loop() :

const game_loop = () => {
  render_board();
  check_board_complete();
};

Nyní místo volání render_board() po každém pohybu hráče nebo počítače zavoláme game_loop().

Nyní musíme připravit hráče a počítač tak, aby se po dokončení desky nemohli přesunout. To se provede takto:

const addPlayerMove = e => {
  if (!board_full && play_board[e] == "") {
    play_board[e] = player;
    game_loop();
    addComputerMove();
  }
};

const addComputerMove = () => {
  if (!board_full) {
    do {
      selected = Math.floor(Math.random() * 9);
    } while (play_board[selected] != "");
    play_board[selected] = computer;
    game_loop();
  }
};

Hra se nyní hraje dobře a nevyskytla se žádná chyba javascriptu. Nyní musí být hra schopna zjistit, zda existuje nějaký vítěz, a podle toho jednat.

Takže jsem deklaroval funkci pro kontrolu vítěze jménem check_for_winner . Tato funkce bude využívat funkci s názvem check_match [deklarováno později]. S pomocí check_match tato funkce určí, zda vyhrál hráč nebo počítač nebo zda se zápas změnil v remízu. Pamatujte, že h2 s id winner . Nyní je čas si ji pořídit a nastavit její text také podle vítěze. Funkce check_for_winner aktuálně vypadá takto:

const winner_statement = document.getElementById("winner");
const check_for_winner = () => {
  let res = check_match();
  if (res == player) {
    winner.innerText = "Winner is player!!";
    winner.classList.add("playerWin");
    board_full = true;
  } else if (res == computer) {
    winner.innerText = "Winner is computer";
    winner.classList.add("computerWin");
    board_full = true;
  } else if (board_full) {
    winner.innerText = "Draw!";
    winner.classList.add("draw");
  }
};

Zde jsem nastavil vnitřníText příkazu vítěz* podle vítěze a podle toho přidal nějakou třídu do h2. Tyto třídy mají některé vlastnosti css takto definované v style.css :

.playerWin {
  color: green;
}

.computerWin {
  color: red;
}

.draw {
  color: orangered;
}

Nyní musíme definovat funkci kontroly shody. V tic-tac-toe jsou možné čtyři druhy shody:

  1. V řadě
  2. Ve sloupci
  3. &4. Dvě úhlopříčky

Abychom porozuměli situaci, nakreslete index z pole desky v herní oblasti:

Board:

  0 | 1 | 2
 ______ ___
  3 | 4 | 5
 ______ ___
  6 | 7 | 8

Abychom tedy zkontrolovali shodu řádků, musíme zkontrolovat index i, i+1 a i+2 pro prvky 0,3,6. Použil jsem tedy smyčku, abych zkontroloval, zda jsou tyto tři stejné a byly vyplněny hráčem nebo počítačem. Protože se tato kontrola opakuje pro všechny, tak jsem pro tuto kontrolu tří bloků deklaroval malou funkci, kde předávám index a dostanu výsledek v boolean, pokud existuje shoda:

const check_line = (a, b, c) => {
  return (
    play_board[a] == play_board[b] &&
    play_board[b] == play_board[c] &&
    (play_board[a] == player || play_board[a] == computer)
  );
};

Nyní zpět ke kontrolnímu řádku. Jak jsem řekl dříve, funkce check_match vrátí symbol toho, kdo má na hrací ploše trojku. Takže kód pro kontrolu řádku bude vypadat takto:

for (i = 0; i < 9; i += 3) {
  if (check_line(i, i + 1, i + 2)) {
    return play_board[i];
  }
}

U sloupců potřebujeme zkontrolovat index i, i+3 a i+6 pro prvky 0,1,2. Kód vypadá takto:

for (i = 0; i < 3; i++) {
  if (check_line(i, i + 3, i + 6)) {
    return play_board[i];
  }
}

Nyní zbývá kontrola úhlopříček, kterou lze snadno provést kontrolou:0,4,8 a 2,4,6:

if (check_line(0, 4, 8)) {
  return play_board[0];
}
if (check_line(2, 4, 6)) {
  return play_board[2];
}

Nyní kompletní kód check_match vypadá takto:

const check_match = () => {
  for (i = 0; i < 9; i += 3) {
    if (check_line(i, i + 1, i + 2)) {
      return play_board[i];
    }
  }
  for (i = 0; i < 3; i++) {
    if (check_line(i, i + 3, i + 6)) {
      return play_board[i];
    }
  }
  if (check_line(0, 4, 8)) {
    return play_board[0];
  }
  if (check_line(2, 4, 6)) {
    return play_board[2];
  }
  return "";
};

Ve své verzi kódu jsem pomocí document.querySelector() změnil pozadí odpovídajících bloků na zelené . Tuto část nechám jako cvičení na čtenáři.

Nyní můžeme přidat check_for_winner do naší herní smyčky, jak se provádí v každém kroku:

const game_loop = () => {
  render_board();
  check_board_complete();
  check_for_winner();
};

Nyní poslední věc, která zbývá implementovat, je funkce reset_board. Zde udělám board prázdný, nastavím board_full na false a odstraním text a styling z h2 id winner. A se všemi těmito změnami vykresluji desku:

const reset_board = () => {
  play_board = ["", "", "", "", "", "", "", "", ""];
  board_full = false;
  winner.classList.remove("playerWin");
  winner.classList.remove("computerWin");
  winner.classList.remove("draw");
  winner.innerText = "";
  render_board();
};

Jedna věc, kterou je třeba mít na paměti při psaní tohoto kódu, je, že můžete volat funkci v JavaScriptu, pokud je již deklarována. Takže konečný kód vypadá takto:

const player = "O";
const computer = "X";

let board_full = false;
let play_board = ["", "", "", "", "", "", "", "", ""];

const board_container = document.querySelector(".play-area");

const winner_statement = document.getElementById("winner");

check_board_complete = () => {
  let flag = true;
  play_board.forEach(element => {
    if (element != player && element != computer) {
      flag = false;
    }
  });
  board_full = flag;
};

const check_line = (a, b, c) => {
  return (
    play_board[a] == play_board[b] &&
    play_board[b] == play_board[c] &&
    (play_board[a] == player || play_board[a] == computer)
  );
};

const check_match = () => {
  for (i = 0; i < 9; i += 3) {
    if (check_line(i, i + 1, i + 2)) {
      return play_board[i];
    }
  }
  for (i = 0; i < 3; i++) {
    if (check_line(i, i + 3, i + 6)) {
      return play_board[i];
    }
  }
  if (check_line(0, 4, 8)) {
    return play_board[0];
  }
  if (check_line(2, 4, 6)) {
    return play_board[2];
  }
  return "";
};

const check_for_winner = () => {
  let res = check_match()
  if (res == player) {
    winner.innerText = "Winner is player!!";
    winner.classList.add("playerWin");
    board_full = true
  } else if (res == computer) {
    winner.innerText = "Winner is computer";
    winner.classList.add("computerWin");
    board_full = true
  } else if (board_full) {
    winner.innerText = "Draw!";
    winner.classList.add("draw");
  }
};

const render_board = () => {
  board_container.innerHTML = ""
  play_board.forEach((e, i) => {
    board_container.innerHTML += `<div id="block_${i}" class="block" onclick="addPlayerMove(${i})">${play_board[i]}</div>`
    if (e == player || e == computer) {
      document.querySelector(`#block_${i}`).classList.add("occupied");
    }
  });
};

const game_loop = () => {
  render_board();
  check_board_complete();
  check_for_winner();
}

const addPlayerMove = e => {
  if (!board_full && play_board[e] == "") {
    play_board[e] = player;
    game_loop();
    addComputerMove();
  }
};

const addComputerMove = () => {
  if (!board_full) {
    do {
      selected = Math.floor(Math.random() * 9);
    } while (play_board[selected] != "");
    play_board[selected] = computer;
    game_loop();
  }
};

const reset_board = () => {
  play_board = ["", "", "", "", "", "", "", "", ""];
  board_full = false;
  winner.classList.remove("playerWin");
  winner.classList.remove("computerWin");
  winner.classList.remove("draw");
  winner.innerText = "";
  render_board();
};

//initial render
render_board();

Závěr

Tato aplikace ještě není dokončena. Stále existuje mnoho způsobů, jak tuto aplikaci vylepšit. Některé ze zřejmých jsou

  1. Přidání režimu Player vs. Player
  2. Udělejte počítač chytřejším pomocí umělé inteligence.

Takže se pokusím udělat tyto věci možná v budoucím příspěvku. Tuto aplikaci najdete zde.