Blog | Okomentovaný zdroják piškvorek

Okomentovaný zdroják piškvorek

Někteří jste si jistě stáhli piškvorky a zahráli si. Zjistili jste nejspíš, že hra je najednou pekelně těžká. No a chtěli byste piškvorky třeba upravit, nebo se podívat, jak se taková hra udělá v Clojure?

Pojďme si projít zdrojáky.

(ns piskvorky.core
  (:require [clojure.string :as s])
  (:gen-class))

Celá hra se vešla do jednoho namespace piskvorky.core. protože kód nepřesáhl 100 řádek, necítil jsem potřebu zavádět další ns. Direktiva gen-class vytvoří třídu, takže vygenerovaný jar souboru půjde pustit.

(defn usage []
  (println "Ahoj v piskvorkach naslepo.\nPovolene prikazy jsou:\nnew - nova hra\nquit - konec\n[a-i][0-9] - tah na pole, kde rada je pozice a, b, c, d, e, f, g, h, i. Sloupec je 1 ... az 9.\nformat zapisu je napr. e5\nZacina x"))

Nejspíš nepotřebuje komentář. Snad jen to, že dávat regulární výrazy se moc nehodí do uživatelského manuálu hry, kterou ovládají i pětileté děti.

(defn make-board []
  (vec (repeat 9 (vec (repeat 9 :nothing)))))

Vygeneruje hrací pole tvořené vektorem 9x9. Samozřejmě mohl bych snadno hrací pole reprezentovat i jen jako pole 91 čísel, nebo dokonce jen jako bitset 182 bitů (což je jasná přeoptimalizace, na server s 96 giga RAM by se vešlo 3 a půl miliardy her, což při běžné dynamice hry v žádném případě neprojde síťovkou.

(defn command->position [command]
  (if (= 2 (count command))
    (let [fst (subs command 0 1)
          snd (subs command 1 2)]
      (if (and (contains? #{"a" "b" "c" "d" "e" "f" "g" "h" "i"} fst)
               (contains? #{"1" "2" "3" "4" "5" "6" "7" "8" "9"} snd))
        [
         ({"a" 0 "b" 1 "c" 2 "d" 3 "e" 4 "f" 5 "g" 6 "h" 7 "i" 8} fst)
         ({"1" 0 "2" 1 "3" 2 "4" 3 "5" 4 "6" 5 "7" 6 "8" 7 "9" 8} snd)]
        :error))
    :error))

Funkce, která zadané souřadnice převede z formátu a1 na vektor [0 0], tzn. na přímou adresu pole. Tato funkce zároveň validuje, čímž zdvojuje data. S troškou úsilí by se dala funkce ještě zkrátit tím, že bych si hash uložil od "proměnné", kterou bych pak používal.

(defn contains-5-iter [[fst & coll] active actual-count]
  (if (= fst active)
    (if (and (= 4 actual-count) (not= active :nothing))
      true
      (recur coll active (inc actual-count)))
    (if (= 0 (count coll))
      false
      (recur coll fst 1))))

Rekurzivní funkce, která vrací, zda zadaná kolekce obsahuje řadu 5 stejných. Zajímavé je rozpadání pole do fst a zbytku coll pomocí [fst & coll] v argumentech funkce.

(defn contains-5 [coll]
  (if (> (count coll) 4)
    (contains-5-iter (rest coll) (first coll) 1)
    false))

Funkce, která zjistí, zda kolekce obsahuje řadu 5 stejných v pohodlné formě.

(defn take-9-around [board position xfn yfn]
  (let [xs (map xfn (range -4 5))
        ys (map yfn (range -4 5))
        positions (map (fn [x y] [x y]) xs ys)
        valid-positions (filter (fn [[x y]] (and (>= x 0) (<= x 8) (>= y 0) (<= y 8))) positions)
        ]
    (map (partial get-in board) valid-positions)))

(defn won [board active-player position]
  (or
    (contains-5 (take-9-around board position (partial + (first position)) (fn [_] (second position)))) ; L < - > R
    (contains-5 (take-9-around board position (fn[_] (first position)) (partial + (second position)))) ; U < - > D
    (contains-5 (take-9-around board position (partial + (first position)) (partial + (second position)))) ; LD <-> UR
    (contains-5 (take-9-around board position (partial + (first position)) (partial - (second position)))))) ; LU <-> DR

To, že někdo vyhrál, poznáme tak, že kolem bodu, kde hráč umístil svůj tah, uděláme do všech stran vektory o 9 prvcích (tah + 4 na každou stranu). Pak odstraníme všechny mimo hrací plochu (řádka začínající valid-positions) a použijeme funkci contains-5, kterou jsme si definovali dříve.

Zajímavé je použití funkcí, které čekají nagenerované číslo -4, -3 až 4 a které vrací odpovídající souřadnici.

(defn turn [board active-player position]
  (assoc-in board position active-player))

Tah jen modifikuje hrací plochu.

(defn full-board [board]
  (not (contains? (set (flatten board)) :nothing)))

Jak zjistit, že je hrací plocha plná? Zde tak, že udělá z vektoru vektorů jen jednorozměrné pole. To se převede na set, což je datová struktura, která může obsahovat pouze unikátní prvky. Pokud je pole plné, obsahuje pouze :x a :o, což jsou tahy a neobsahuje :nothing, což je prázdné políčko.

Opět by šlo toto vyřešit rychleji s pomocí map a funkce not-any?. Toto řešení je zcela dostatečné pro 1 hráče. V případě, že by bylo hrací pole reprezentované jako 1 pole, stačil by kód:


(defn full-board [coll]
        (not-any? (partial = :nothing) coll))

; nebo v point-free formatu

(def full-board (partial not-any? (partial = :nothing)))

Jedeme dál.

(def next-player
  {:x :o
   :o :x})

Tady se dostáváme na krásnou vlastnost Clojure. Hash je zároveň funkce a pokud ho zavoláme s argumentem, který je roven jednomu z jeho klíčů, vrátí hodnotu. Takže jsme si vytvořili funkci (protože funkce je relace, která pro každou hodnotu z definičního oboru přiřazuje právě jednu hodnotu z oboru hodnot, což je pravda).

(defn already-taken [board position]
  (not= :nothing (get-in board position)))

Pole je volné, pokud na něm je nastaveno :nothing.

(defn print-board [board]
  (println (s/reverse (s/join "\n" (map (fn [row]
                                          (s/reverse (s/join " "
                                                             (map (fn [item]
                                                                    (case item
                                                                      :x "x"
                                                                      :o "o"
                                                                      :nothing "_")) row)
                                                             ))) board)))))

Na konci hry chceme vytisknout hrací pole. Tato funkce převede :x na "x", :o na "o" a :nothing na "_". To vše spojí do stringu, který se poté vytiskne.

(defn game-loop [board active-player game-status]
  (do
    (println "Hrac" (name active-player) " ")
    (let [command (read-line)
          position (command->position command)
          args (cond (= command "new")              (do (println "Nova hra") (list (make-board) :x :active))
                     (= command "quit")             (println "Navidenou")
                     (= command "board")            (do (print-board board) (list board active-player game-status))
                     (= position :error)            (do (println "Tah ve spatnem formatu")
                                                        (list board active-player game-status))
                     (= game-status :complete)      (do (println "Hra dokoncena, dej \"new\" pro novou")
                                                        (list board active-player game-status))
                     (already-taken board position) (do (println "Pole je zabrano, hraj znovu")
                                                        (list board active-player game-status))
                     true (let [new-board (turn board active-player position)]
                            (if (won new-board active-player position) (do
                                                        (println "VYHRA! Gratulace hraci " active-player)
                                                        (print-board new-board)
                                                        (list new-board (next-player active-player) :complete))
                              (if (full-board new-board)
                                (do (println "Remiza, hraci pole zaplneno") (list new-board active-player) :complete)
                                (do (println "Ok") (list new-board (next-player active-player) game-status))))))]
      (when args (recur (first args) (second args) (nth args 2))))))

A tady je samotná herní smyčka. Je snadná, za zmínku stojí jen to, že do args se vždy dostane buď nil, aplikace tím má zkončit, nebo pole s parametry pro rekurzi. Samotná rekurze je na konci funkce.

(defn init []
  (usage)
  (game-loop (make-board) :x :active))

(defn -main []
  (init))

Inicializace a spuštění aplikace. -main a init by šly spojit.


Na cca 90 řádcích jsem napsal primitivní hru. Věřím, že byste se i v jiných jazycích dostali na podobné číslo. Co mi přijde důležité, že jste se podívali na některé věci, které jsou v Clojure možné.

Programování

Předejte zkušenosti i dalším a sdílejte tento článek!

Předchozí článek


Jiří Knesl
Business & IT konzultant

Jiří Knesl poprvé začal programovat v roce 1993. Od té doby, díky skvělým učitelům a později zákazníkům, měl možnost neustále růst v oboru vývoje webových aplikací a informačních systémů. v roce 2002 se přidal zájem o ekonomii a v roce 2006 o organizaci práce. Vším tím se konstantně profesně zabývá jak ve svém podnikání, tak i u zákazníků. Za posledních 5 let vydal na tato témata přes 400 článků.

Prohlédněte si moje reference

Mám zkušenosti z rozsáhlých projektů pro korporace, velké podniky, střední i malé firmy, ale i pro startupy v cloudu. Zvyšoval jsem jejich know-how, pomáhal nastavovat jejich organizační strukturu, byl lektorem a mentorem v náročných situacích. Podívejte se, jak vidí můj přínos samotní klienti.

Sledujte mé postřehy na sociálních sítích