Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Zróbmy sobie grę... w CSS

Kaskadowe arkusze stylów (ang. Cascading Style Sheets, w skrócie CSS) to język służący do opisu formy prezentacji (wyświetlania) stron WWW.

Takie zdanie można zobaczyć wpisując hasło "CSS" w Wikipedię. Co jednak gdyby definicję tę trochę nagiąć i stworzyć coś co na pierwszy rzut oka jest czymś więcej niż jedynie opisem formy prezentacji? Znacie grę Saper? Jest to zdecydowanie moja ulubiona mini gra, dlatego też postanowiłem wziąć ją na warsztat i stworzyć własną wersję. I nie byłoby w tym absolutnie nic nadzwyczajnego, ale postanowiłem wykorzystać do tego celu HTML i CSS. Tylko. Żadnego Javascriptu. Nic.

Dla niecierpliwych przygotowałem też możliwość podglądu finalnego efektu.

Aby formalności stało się zadość wspomnę jeszcze, że nie gwarantuje działania na wszystkich przeglądarkach, dlatego użytkownicy IE6 mogą się rozczarować.

Ale od początku

Zobaczmy z czego składa się klasyczna plansza Sapera. W naszym małym projekcie pominiemy zegar (choć również da się go zrobić w CSS) żeby niepotrzebnie nie rozbudowywać wpisu. Najważniejsza oczywiście jest siatka pól oraz możliwość odkrywania/zaznaczania ich. Skupmy się na samym odkrywaniu pól i zastanówmy się jaki element HTML pozwala się zaznaczać i odznaczać. Szczęśliwie istnieje checkbox i to na nim będziemy opierać całą grę.
Ale, ale! Ja chcę żeby mój Saper wyglądał ładnie, czy checkboxa da się odpowiednio ostylować?
Mamy rok 2016 (no...prawie 2017). Ludzie latają w kosmos, nasza sonda Voyager 1 przebywa w przestrzeni międzygwiezdnej a najmniejszych robotów nie da się zobaczyć gołym okiem. Czy wobec tego są dla rodzaju ludzkiego jakieś bariery? Owszem - ostylowanie checkboxa. Niestety nieważne ile stylów by do niego nie napisać to przeglądarka i tak wyświetli ten element strony po swojemu. W zamierzchłych czasach używało się do tego celu javascriptu, ale heloł - miało być bez niego!
Odkrył prosty sposób na ostylowanie checkboxa bez użycia Javascriptu! Naukowcy go nienawidzą! Szok! [ZOBACZ ZDJĘCIA]
Oczywiście żartuję - sposób nie jest niczym nowym i jest dość powszechnie stosowany - cały trick polega na ukryciu samego checkboxa oraz odpowiednim ostylowaniu dołączonego elementu label.
Dodajmy więc checkboxa i zdefiniujmy jego wygląd tak, aby wyglądał jak kwadrat: //index.pug doctype html html(lang="en") head meta(charset="UTF-8") link(href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet") link(rel="stylesheet" href="styles/style.css") title MineSweeper body input#foobar(type="checkbox") label(for="foobar") Click! //style.scss #foobar { display: none; & + label { border: 1px solid black; width: 50px; height: 50px; display: block; cursor: pointer; } &:checked + label { background: gray; } }
Już się z tego tłumaczę - zamiast HTML będę używał PUG'a który upraszcza składnie standardowego kodu strony i wprowadza parę innych przydatnych sztuczek. Podobnie zamiast CSS wolę używać SASS. Nie ma w tym żadnego oszustwa, bo finalnie oba te języki są kompilowane do swoich normalnych odpowiedników. Mógłbym wszystko robić bezpośrednio w standardowym HTML i CSS, ale to tylko niepotrzebne zużycie klawiatury. Zresztą na repozytorium z gotowym projektem znajdziesz kod zarówno przed jak i po kompilacji.
A oto szalenie interesujący rezultat:

Wincyj pól!

Gra w sapera z planszą 1x1 byłaby interesująca niczym kolejny artykuł porównujący Linuxa i Windowsa, dlatego spróbujmy dodać ich kilka więcej. Jako że z natury jestem leniwy, to zamiast tworzyć to wszystko ręcznie, wykorzystam pętle dostępne zarówno w PUG'u jak i SCSS'ie (już rozumiesz czemu zdecydowałem się na preprocesory?). Powtórzę się znowu - nie ma w tym nic zbereźnego, równie dobrze mógłbym użyć metody Copypastego. Abym mógł wygodnie sterować wszystkimi polami, najpierw wypisze sobie wszystkie niewidzialne checkboxy, a dopiero potem odpowiadające im labele. Dodatkowo dla większego porządku podzielę sobie moje style na kilka plików. //index.pug - var fields = [1,0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,0,1,0,1,0,0,1,1,1] body .wrapper .game .board each field, index in fields input.field(id="field" + index, type="checkbox") each field, index in fields label.field(id="label" + index, for="field" + index) //style.scss @import "data"; @import "presentation"; //_data.scss $fields: 1,0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,0,1,0,1,0,0,1,1,1; //_presentation.scss input.field { display: none; } label.field { border: 1px solid black; width: 50px; height: 50px; display: block; cursor: pointer; } @for $i from 0 through length($fields)-1 { #field#{$i}:checked ~ #label#{$i} { background: gray; } } Dorzućmy do tego parę nudniejszych formuł takich jak wyśrodkowanie, zaokrąglenie rogów czy flexbox i uzyskamy pierwszy zarys pól:

Bombowo! Ale co z zaznaczaniem bomb?

Saper w którym możemy tylko odkrywać pola to dalej nie jest gra marzeń. Dlatego zajmiemy się teraz trybem oznaczania bomb. W tym celu stworzymy lustrzaną siatkę jak już istniejąca, nałożymy ją na plansze i domyślnie ukryjemy. Dodatkowo ponad planszą dodajmy przyciski służące do zmiany trybu (odkrywanie/zaznaczanie) w formie elementów radio. To na podstawie tego który z nich jest zaznaczony będziemy pokazywać lub ukrywać siatkę służącą do zaznaczania min. Jeśli wybrana będzie opcja "kopania" to ukryte będą pola z nowo dodanej siatki (poza już zaznaczonymi kratkami - te powinny być widoczne zawsze). //index.pug .game input.action-type#dig(type="radio" name="actionType" checked) label.action-type(for="dig") DIG input.action-type#mark(type="radio" name="actionType") label.action-type(for="mark") MARK .board each field, index in fields input.field(id="field" + index, type="checkbox") input.field(id="mine-field" + index, type="checkbox") each field, index in fields label.field(id="label" + index, for="field" + index) .board-mines each field, index in fields label.field(id="mine-label" + index for="mine-field" + index) Style dotyczące zachowań będziemy umieszczać w osobnym pliku który trzeba dopisać do agregującego importy style.scss //style.scss @import "data"; @import "logic"; @import "presentation"; //logic.scss .board { #mark:checked ~ label.field{ pointer-events: none; } .board-mines{ visibility: hidden; margin-top: 0; label { visibility: hidden; #mark:checked ~ & { visibility: visible; } #dig:checked ~ & { pointer-events: none; } } } } @for $i from 0 through length($fields)-1 { #mine-field#{$i}:checked ~ .board-mines #mine-label#{$i} { visibility: visible; } } //presentation.scss // pomijam mniej ważne style, dotyczące wyglądu @for $i from 0 through length($fields)-1 { #field#{$i}:checked ~ #label#{$i} { background: gray; } #mine-field#{$i}:checked ~ .board-mines #mine-label#{$i} { background: yellow; } }
Zostały do obsłużenia jeszcze trzy niuanse:
  • Nie powinniśmy móc zaznaczyć na żółto pola szarego (czyli postawić flagi na odkrytym polu)
  • Szare pole musi być szare na zawsze (nie można zakopać odkopanego pola)
  • Nie można zaznaczyć żółtego pola jako szare (czyli odkopać pola oznaczonego jako bomba)
Wbrew pozorom sprawa banalna, wystarczy dodać właściwość pointer-events: none stosując odpowiednie selektory: //logic.scss @for $i from 0 through length($fields)-1 { #mine-field#{$i}:checked ~ .board-mines #mine-label#{$i} { visibility: visible; } #mine-field#{$i}:checked ~ #label#{$i} { pointer-events: none; } #field#{$i}:checked ~ .board-mines #mine-label#{$i} { pointer-events: none; } #field#{$i}:checked ~ #label#{$i} { pointer-events: none; z-index: 5; } }

Dobra dobra... gdzie te bomby?

Mamy planszę po której można klikać i... w sumie to tyle. Niewielka satysfakcja, więc pora podnieść poprzeczkę i dodać możliwość wybuchnięcia użytkownika za odkopanie bomby. Dodajmy więc do naszego PUG'a element sygnalizujący przegraną: //index.pug - var fields = [1,0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,0,1,0,1,0,0,1,1,1] // (...) form.board // (...) .loser-screen button(type="reset") Uh... :( One more time? Przycisk typu reset wyczyści nam cały formularz (czyli naszą planszę) i przywróci grę do stanu wyjściowego. Co uważniejsi zauważyli pewnie dziwny rozkład tablicy fields - czyżby jakiś przekaz binarny? Nic bardziej mylnego! 1 oznacza bombę, 0 oznacza jej brak, a indeks w tablicy to numer pola (wszak iterujemy po nich w pętli, nieprawdaż?). Dodajmy więc informację do pól o ich bombowości: //index.pug .board each field, index in fields input.field(id="field" + index, data-info=field == 1 ? 'mine' : 'yay', type="checkbox") input.field(id="mine-field" + index, type="checkbox") each field, index in fields label.field(id="label" + index, data-info=field == 1 ? 'mine' : 'yay', for="field" + index) Teraz w naszym silniku gry, to znaczy w stylach, możemy stworzyć prosty "warunek" - loser-screen ma się pojawić kiedy poprzednio został zaznaczony checkbox z odpowiednią informacją wpisaną w atrybut data-info: //_logc.scss input[data-info~="mine"]:checked ~ .loser-screen{ visibility: visible; z-index: 10; } Parę dodatkowych, niewiele znaczących stylów i otrzymujemy ekran przegranego po kliknięciu w złe pole:

Daj mi podpowiedź, choćby najmniejszą!

Saper nie zdobyłby takiej popularności gdyby chodziło w nim jedynie o zgadywanie miejsc gdzie są bomby, dlatego dodajmy podpowiedzi dla użytkownika. Podobnie jak w klasycznej wersji gry, umieścimy liczby na odkrytych polach, które to będą oznaczać ilość bomb na polach sąsiednich. Jako, że układ planszy znamy z góry nie powinno być to trudne zadanie, jednak skorzystajmy z tego, że SASS pozwala na pozory programowania i napiszmy algorytm działający dla dowolnej planszy. Nie ma sensu wspinać się tutaj na wyżyny algorytmiki - taki skrawek kodu wystarczy w zupełności: //_data.scss $fieldsPerRow: 5; $fields: 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1; $minesAround: (); @function neighborExists($field, $direction) { @if $direction == 'up' { @return $field - $fieldsPerRow > 0 } @if $direction == 'down' { @return $field + $fieldsPerRow <= length($fields) } @if $direction == 'right' { @return $field % $fieldsPerRow != 0 } @if $direction == 'left' { @return $field % $fieldsPerRow != 1 } } @function hasMine($field) { @return nth($fields, $field) == 1 } @for $i from 1 through length($fields) { $mines: 0; @if neighborExists($i, 'up') and hasMine($i - $fieldsPerRow) { $mines: $mines + 1; } @if neighborExists($i, 'down') and hasMine($i + $fieldsPerRow) { $mines: $mines + 1; } @if neighborExists($i, 'right') and hasMine($i + 1) { $mines: $mines + 1; } @if neighborExists($i, 'left') and hasMine($i - 1) { $mines: $mines + 1; } @if neighborExists($i, 'left') and neighborExists($i - 1, 'up') and hasMine($i - 1 - $fieldsPerRow) { $mines: $mines + 1; } @if neighborExists($i, 'right') and neighborExists($i + 1, 'up') and hasMine($i + 1 - $fieldsPerRow) { $mines: $mines + 1; } @if neighborExists($i, 'left') and neighborExists($i - 1, 'down') and hasMine($i - 1 + $fieldsPerRow) { $mines: $mines + 1; } @if neighborExists($i, 'right') and neighborExists($i + 1, 'down') and hasMine($i + 1 + $fieldsPerRow ) { $mines: $mines + 1; } $minesAround: append($minesAround, $mines, comma) } //_presentation.scss @for $i from 0 through length($fields)-1 { #field#{$i}:checked ~ #label#{$i} { background: #C2C3C5; line-height: 50px; color: #748F28; &[data-info~="yay"]{ &:after { content: '' + nth($minesAround, $i+1) } } } } Brzydkie? Owszem - ale działa, a my nie tworzymy tutaj sterownika do rakiety, tylko Sapera w CSS.

Jedni przegrywają aby inni mogli wygrać!

Co to za gra w której można przegrać, a nie można wygrać? Skoro zrobiliśmy ekran przegranego, to analogiczny ekran dla wygranego nie powinien być dużym wyzwaniem. Aby uznać zwycięstwo musimy sprawdzić czy wszystkie niebombowe pola są odkryte: $yayList: (); @for $i from 1 through length($fields) { $yayList: append($yayList, unquote('#field#{$i}:checked ~ '), space); } #{$yayList} .winner-screen { visibility: visible; z-index: 10; }

Daleko jeszcze do końca?

Do końca wpisu jeszcze kawałek, ale pytanie jak daleko jeszcze do końca gry? Przydałby się licznik min pozostałych do znalezienia... Tylko czy w CSS można coś zliczać? Okazuje się że można dzięki "zmiennej" counter.
Zdefiniujmy więc minesCount i fieldsCount oraz odpowiednio manipulujmy nimi w zależności od zaznaczonych pól na planszy: //_data.scss $minesCount: 0; @each $field in $fields { @if $field == 1 { $minesCount: $minesCount + 1 } } //_logic.scss body { counter-reset: minesCount fieldsCount; } .game-stats{ .fields { &:after { content: 'Digged fields: ' counter(fieldsCount) '/' + (length($fields) - $minesCount) } } .mines { &:after { content: 'Marked mines: ' counter(minesCount) '/' + $minesCount } } } @for $i from 0 through length($fields)-1 { #mine-field#{$i}:checked ~ .board-mines #mine-label#{$i} { counter-increment: minesCount } #field#{$i}:checked ~ #label#{$i} { counter-increment: fieldsCount } } Na koniec jeszcze kilka pociągnięć pędzlem, żeby całość lepiej się prezentowała...

To już jest koniec...

Jeśli dotrwałeś do tego momentu - należą się wyrazy uznania. Być może ktoś spyta - po co takie coś? Przecież plansza jest ciągle taka sama. Odpowiedź jest prosta: po nic ;) Ot ciekawostka, przy okazji pokazująca kilka mniej znanych właściwości CSS. Czy dało się to zrobić lepiej? Tak, jestem przekonany że tak, ale z założenia miałem poświęcić na to nie więcej niż 2 wieczory i oto rezultat. Daleki od ideału ale przekazujący ideę. We wpisie z wiadomych względów umieściłem tylko strzępki kodu dlatego też pełny przykład umieszczam na moim GitHubie.

Świąteczny bonus!

Jako że święta za pasem, przygotowałem też świąteczną odsłonę gry. W tym celu zdefiniowałem dodatkowy zestaw stylów i stosuje je jeśli zaznaczony został odpowiedni checkbox w lewym górnym rogu ekranu. W tym miejscu podziękowania dla mojego znajomego, Radka, za przygotowanie grafik prezentów - wyszły bombowo ;) //_layout.christmas.scss #christmas:checked ~ .wrapper { background: url('../img/christmas/bg.jpg') no-repeat; background-size: cover; .board, .board-mines{ width: 290px; } label.field{ border: none; margin: 4px; } label.field { background: url('../img/christmas/field.png') no-repeat; background-size: cover; } label.action-type { border: 1px solid #550900; @include background(radial-gradient(#EA6165, #a30911)); } }
 

internet programowanie inne

Komentarze

0 nowych
  #1 22.12.2016 18:06

Żaden użytkownik IE6 nie będzie niezadowolony, bo... ich nie ma! :D . Fajnie ci wyszedł saper.

Berion   15 #2 22.12.2016 18:35

Jeszcze nie przebywa. Wciąż mozolnie prze naprzód na krańcach Obłoku Oorta. ;)

A co do gry: fajne!

kubut   18 #3 22.12.2016 19:00

@Berion: jesteś pewien? Być może coś źle rozumiem ale wydaje mi się że już wkroczyła w przestrzeń międzygwiezdna http://www.jpl.nasa.gov/news/news.php?release=2013-277
Jestem akurat poza domem więc być może coś źle zrozumiałem czytając na szybko w tramwaju ;-)

  #4 22.12.2016 19:12

A jednak się da! Wielkie wyrazy uznania!

karol221-10   12 #5 22.12.2016 19:19

Nie interesuję się co prawda technologiami webowymi, więc mogę się mylić. Ale nie mogę oprzeć się wrażeniu, że to jest ciągle tylko sztuka dla sztuki.
Oczywiście sam pomysł i metoda wykonania są bardzo fajne :)

Autor edytował komentarz w dniu: 22.12.2016 19:19
kubut   18 #6 22.12.2016 19:23

@karol221-10: dokładnie,to jest sztuka dla sztuki ;-)

bravo   18 #7 22.12.2016 20:17

Fajna zabawka ;-)

Berion   15 #8 22.12.2016 20:39

@kubut: W 2013 roku tak twierdzili, pół roku później doszli do wniosku że jeszcze są na naszym podwórku >> mem >> scientists ;]

Autor edytował komentarz w dniu: 22.12.2016 20:42
pyXelr   5 #9 22.12.2016 21:02

Design wyglądem przypomina guziki na launchpadzie.

  #10 22.12.2016 21:33

"Naukowcy do nienawidzą!"

Chyba "go"

Shaki81 MODERATOR BLOGA  38 #11 22.12.2016 21:36

@karol221-10: Dokładanie, a dlaczego Kubut to zrobił? Bo chciał i mógł, niczego więcej nie należy się tu doszukiwać.

Choć programowanie to nie moja działka, ale opracowanie dobre.

kubut   18 #12 22.12.2016 22:39

@Berion: O widzisz... Nie śledzę tematu, info pobrałem z Wikipedii, a jako że było poparte linkiem do strony NASA to wyglądało legitnie :)

Jim1961   7 #13 22.12.2016 22:42

A dlaczego ten checkbox obok "Christmas Time!" taki ... zwyczajny? :P

kubut   18 #14 22.12.2016 22:43

@Jim1961: Bo już mi się nie chciało... :D

kubut   18 #15 22.12.2016 22:44

@pyXelr: Zbieżność wyglądu przypadkowa :)

  #16 22.12.2016 23:07

Temat zahacza o ważne pytanie: czy CSS jest Turing kompletny? Jeśli byłby można byłoby zakodować w nim wszystko. Na sieci są dyskusje i odpowiedź wcale nie jest taka oczywista.

Wokuo   7 #17 23.12.2016 01:11

Czyli programista html/css nie jest tak znowu z dupy wzięty. Całe życie w błędzie...

gowain   19 #18 23.12.2016 01:31

Szacun, że Ci się chciało :)

pilot67   4 #19 23.12.2016 09:02

"Mógłbym wszystko robić bezpośrednio w standardowym HTML i CSS [...]"

wtedy trafiłbyś do o rząd większego kręgu odbiorców.

"[...] ale to tylko niepotrzebne zużycie klawiatury. "

tak mawiał mój wykładowca (może i Twój ;-) ) - miał ogromną wiedzę i zerowe zdolności przekazania jej innym.

kubut   18 #20 23.12.2016 09:33

@pilot67: O ile PUG jest faktycznie mało popularny o tyle z LESS lub SASS (które składniowo są podobne) korzystają właściwie wszystkie osoby na poważnie zajmujące się front-endem (a jeśli nie, to wpis miał być pretekstem do poznania czegoś nowego). Problem z czystym CSS jest taki, że jest go dużo, o wiele za wiele żeby pokazać go (nawet wycinkami) na blogu. Sprawa wyglądałaby inaczej jakby dało się osadzić jakiegoś plunkera lub innego fiddle'a.
SCSS mam ~250 linii podczas gdy skompilowanego CSS jest ponad 1000 ;) Ale jeśli ktoś bardzo chce to specjalnie do repozytorium wrzuciłem też pliki skompilowane - zobacz jak to wygląda w czystej formie i chyba sam przyznasz że jest mniej czytelne.
Natomiast zgodzę się z tym że umiejętności do nauczania to ja nie mam - dlatego wpis bardziej jako ciekawostka niż tutorial :)

  #21 23.12.2016 10:53

Hej jak widzę w tytule litery CS to my się z jednym kojarzy.. uzależnienie;)

BloodyEyes   7 #22 23.12.2016 21:56

Myślałam, że będzie coś o Counter-Strike: Source :P

  #23 23.12.2016 23:00

Kawał dobrej roboty, mógłbyś przetłumaczyć i wrzucić na /r/programming :)

  #24 23.12.2016 23:17

Od kiedy to kompiluje się css albo co gorsza cokolwiek kompiluje się "do css"?!

kubut   18 #25 27.12.2016 23:27

@Anonim (niezalogowany): Odkąd powstały takie języki jak SASS czy LESS? Jak inaczej nazwiesz proces transformacji kodu do CSS?

  #26 28.12.2016 11:15

Jedna sprawa. Brak tu jakiejkolwiek losowości. Wystarczy zwykły bruteforce by znaleźć zwycięską kombinację ruchów :)

kubut   18 #27 28.12.2016 12:05

@Anonim (niezalogowany): Nawet nie potrzeba bruteforce, wystarczy podejrzeć źródło strony :) Nie chodziło tu o grę samą w sobie, a o prezentacje pewnych możliwości :)

Gratulacje!

znalezione maszynki:

Twój czas:

Ogól Naczelnego!
Znalazłeś(aś) 10 maszynek Wilkinson Sword
oraz ogoliłeś(aś) naszego naczelnego!
Przejdź do rankingu
Podpowiedź: Przyciśnij lewy przycisk myszki i poruszaj nią, aby ogolić brodę.