Wprowadzenie

Gry tekstowe, w których użytkownik dostaje opis jakiejś sytuacji i listę możliwości działania, mogą być całkiem zabawne zarówno dla twórców, jak i użytkowników. Podstawowy problem, to wygodny system do ich tworzenia. Jedyną technologią, obsługiwaną na prawie wszystkich urządzeniach, jest Javascript w przeglądarce i na tym opiera się opisywany moduł tekstowych gier przeglądarkowych.

Moduł jest przeznaczony do łatwego tworzenia gier paragrafowych z tym, że nazwa jest mało fortunna, bo paragraf w grze paragrafowej i paragraf w dokumencie HTML to oczywiście są dwie różne rzeczy, a słówko powoduje niejednoznaczność. Dlatego zamiast paragrafu z gry paragrafowej używam zamiennika "sytuacja".

Językiem, w którym tworzy się grę jest Javascript, więc mamy do dyspozycji wszystkie typy i konstrukcje tegoż, np. tablice, obiekty, łańcuchy itd., natomiast gamecore.js definiuje zestaw funkcji przydatnych do łatwego tworzenia gry i automatyzuje czynności typu czyszczenie opisu i opcji po kliknięciu, wykonanie klikniętej akcji i wyprowadzenie na ekran wyników. Kluczowe jest podlinkowanie w dokumencie html do gamecore.js, a kod gry można wpisać albo w osobnym pliku .js i też go podłączyć, albo w samym pliku html. Dokument html musi zawierać elementy div o identyfikatorach gamedesc (dla opisu) i gameoptlist (dla listy opcji) i wywoływać funkcję textgamestart na zdarzeniu body onload. W elementach div ważne są odpowiednie parametry aria, aby czytnik ekranu automatycznie czytał opis sytuacji.

<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Gra tekstowa</title>
<script type="text/javascript" src="gamecore.js"></script>
<script type="text/javascript">
<!--
//Tu umieścimy kod gry.
// -->
</script>
</head>
<body onload="textgamestart()">
<div id="gamedesc" aria-live="assertive" aria-relevant="additions">
</div>
<div id="gameoptlist">
</div>
<noscript>
<p>Używana przeglądarka musi mieć włączoną obsługę JavaScript.</p>
</noscript>
</body></html>

Dla odróżnienia funkcji definiujących sytuacje (paragrafy) gry od funkcji innego przeznaczenia, przyjęto konwencję, że nazwy funkcji sytuacji zaczynają się od prefiksu s_. Wykonywanie gry zaczyna się od sytuacji main, która musi być zdefiniowana w funkcji s_main.

Główne funkcje, to funkcja p(text) wyświetlająca paragraf tekstu, oraz opt(text, akcja) - tworząca przycisk akcji z danym opisem. akcja to może być nazwa sytuacji (bez prefiksu s_) napisana jako łańcuch, czyli w apostrofach lub cudzysłowach, inna funkcja, a w tym funkcja bez nazwy function(){...}, która ma wykonać jakąś akcję, a jednocześnie pozostawić użytkownika w tej samej sytuacji, w której się znajduje.

Pierwsza gra

W miejscu na kod gry, umieszczamy następujący kod:

function s_main()
{
p('Witaj w grze 8-puzle.');
opt('Opis gry','gamedesc');
}//s_main
function s_gamedesc()
{
p('Gra odbywa się na kwadratowej planszy o rozmiarze 3x3 pola (można ustawić większy).');
(...)
opt('Wróć','main');
}//gamedesc

Demo w pliku przyklad01.html

Zmienne skryptu i zmienne stanu gry

W grze używamy zmiennych Javascriptu do zastosowań ogólnych, np. jako liczniki pętli, ale istnieją też zmienne stanu gry - kontenerem na te drugie jest obiekt sgvars, który zostanie zapisany funkcją savegame i wczytany funkcją loadgame. Jeśli chcemy mieć zmienną określającą aktualny poziom gracza, to zamiast ją definiować klasycznie:

var level=1;

piszemy:

sgvars.level=1;

(obiekt sgvars jest wstępnie inicjowany przez gamecore).

Korzystając z konstrukcji if, można definiować opcje, które się pojawią w grze warunkowo.

Dla przykładu, opcja wczytania zapisanej wcześniej gry ma sens tylko pod warunkiem, że przeglądarka obsługuje mechanizm localStorage oraz jakieś stany gry zostały wcześniej zapisane.

Jeśli przeglądarka obsługuje localStorage, to wówczas funkcja textgamestart zdefiniuje trzy funkcje: loadgame, savegame i hassavegame - w przeciwnym wypadku funkcje te nie będą istnieć. Funkcja hassavegame zwraca 1, jeśli jakieś stany gry zostały wcześniej zapisane, czyli dla obsłużenia wczytania stanu gry, wystarczy zapis:

if (window.savegame && window.hassavegame())
opt('Wczytaj zapisaną wcześniej grę',loadgame);

Opcja z dodatkowymi parametrami

W grze "Puzle" ustawienie rozmiaru planszy odbywa się w pętli, ale przekazywanie wartości licznika pętli jako wybranego rozmiaru się nie sprawdzi, gdyż kliknięcie opcji nastąpi już po wykonaniu całej pętli, więc licznik będzie miał zawsze wartość maksymalną.

Aby przekazać do akcji powiązanej z opcją jakąś wartość, należy ją przesłać poprzez opcjonalny trzeci parametr funkcji opt - wówczas dla funkcji akcji, będzie on automagicznie dostępny jako pierwszy argument funkcji akcji. Dla przykładu:

p('Wybierz rozmiar planszy:');
for (var i=2; i<=10; i++)
{
opt(i+' x '+i,
function(newsize){
sgvars.size=newsize;
}, i);
}//for i
opt('Pozostaw bez zmian');

Jak widać, opt może być też wywołana tylko z tekstem, bez funkcji akcji, a w takim przypadku, po kliknięciu tej opcji, nie zostanie wykonana żadna akcja, czyli zostanie tylko powtórzone wykonanie funkcji aktualnej sytuacji (też dzieje się to automatycznie).

Można przekazywać do funkcji akcji dowolnie wiele parametrów, jeśli to konieczne:

for (var x=1; x<3; x++)
for (var y=1; y<3; y++)
opt('Opcja x='+x+', y='+y,
function(x,y)
{
p('x='+x+', y='+y);
},
x,y);
Działający przykład w pliku przyklad04.

Funkcja startdialog

Po kliknięciu opcji wykonywana jest ewentualna akcja tej opcji, oraz powtarzane jest wykonanie funkcji aktualnej sytuacji. Czasem jest to zbędne, gdyż np. po kliknięciu opcji "Zmień rozmiar planszy" w sytuacji "s_start", nie chcemy, by prócz wyboru innego rozmiaru planszy, było coś jeszcze wyświetlane.

Do czasowego zawieszenia automatycznego wykonywania funkcji aktualnej sytuacji, służy funkcja wejścia w tryb dialogu startdialog. Wyjście z dialogu nastąpi albo po wywołaniu enddialog, albo w sytuacji, gdy kliknięta opcja nie wygenerowała żadnych innych opcji, czyli po wybraniu dowolnego rozmiaru planszy, albo opcji "pozostaw bez zmian".

Cała obsługa zmiany rozmiaru planszy wygląda więc następująco:

function s_start()
{
sgvars.level=1;
p('Rozmiar planszy: '+sgvars.size+' x '+sgvars.size+'.');
opt('Zmień rozmiar planszy',
function(){
startdialog();
p('Wybierz rozmiar planszy:');
for (var i=2; i<=10; i++)
{
opt(i+' x '+i,
function(newsize){
sgvars.size=newsize;
}, i);
}//for i
opt('Pozostaw bez zmian');
});//ustaw rozmiar
opt('Wróć','main');
}//start

Demo w pliku przyklad02.html

Generator liczb losowych

Kolejną niezbędną w grze funkcją jest generator liczb losowych - Javascript udostępnia wprawdzie funkcję Math.random, ale zwraca ona wartości zmiennoprzecinkowe od 0 do 1, a częściej przydatne byłoby posiadanie funkcji zwracającej wartości całkowite z danego przedziału - gamecore udostępnia funkcję rand(min, max) zwracającą liczbę całkowitą z zadanego przedziału.

function s_main()
{
p('Test generatora liczb losowych.');
opt('Losuj liczbę od 1 do 20',
function()
{
var los=rand(1,20);
p('Wylosowano '+los+'.');
});
}//s_main

Demo w pliku przyklad03.html

gotosituation - automatyczne przejście do innej sytuacji

Najczęściej przejście do innej sytuacji odbywa się jako rezultat kliknięcia jakiejś opcji, ale czasem może być tak, że zależnie od innych czynników, np. wartości jakichś zmiennych, może nastąpić warunkowe przejście do innej sytuacji. Można to osiągnąć funkcją gotosituation.

if (sgvars.energy<20)
{
p('Bezwolnie zapadasz w głęboki sen.');
gotosituation('dream');
}

function s_dream()
{
var dreamhours=rand(4,8);
gti.inc(dreamhours*3600);
sgvars.energy+=dreamhours*60;
}

Obiekt Gametime

Użyty w poprzednim rozdziale obiekt gti, to przykładowa instancja zdefiniowanego też w gamecore licznika subiektywnego czasu gry (gametime), który możemy zwiększać o zadaną ilość sekund, zależnie od tego ile trwa jakaś czynność wykonana przez postać, ale co ważniejsze - dla obiektu gametime można definiować akcje, które zostaną wykonane, gdy subiektywny czas w grze osiągnie określoną wartość, a też mogą być następnie powtarzane co określoną ilość subiektywnych sekund.

A zatem - wiele może się wydarzyć, gdy postać zapadła w błogi sen trwający losowo od 4 do 8 godzin.

Zaawansowany zapis stanu gry

Stan gry jest zapisywany jako obiekt sgvars skonwertowany metodą JSON.stringify - przy wczytywaniu stanu gry jest dekodowany z tego zapisu. Oznacza to, że zmienne typów prostych, a także tablice i własności obiektów zostaną przechowane, ale np. funkcje akcji obiektu gametime nie zostaną, dlatego przed zapisaniem stanu gry trzeba jakoś skonwertować do typów prostych informacje o ustawionych timerach, które następnie należy przywrócić po wczytaniu stanu gry - konkretne rozwiązania zależą od zastosowanej implementacji i jest to najsłabszy punkt całej sprawy, ale na razie nie znalazłem lepszego rozwiązania.

Do operacji dodatkowych związanych z zapisem i wczytaniem stanu gry służą dwie opcjonalne funkcje, które może definiować gra - beforesavegame i afterloadgame.

Tytuł zapisanego stanu gry

Gra jest zapisywana funkcją savegame, ale aby możliwe było nadanie tworzonemu zapisowi jakiegoś sensownego tytułu, prócz samego numeru zapisu, można tę funkcję wywołać z parametrem stanowiącym opis, który później będzie wyświetlany przy wczytywaniu stanu gry.

if (window.savegame)
opt('Zapisz grę', function()
{
var desc='Poziom '+sgvars.level+', plansza '+sgvars.size+' x '+sgvars.size+'.';
savegame(desc);
});

Warsztat

Na koniec jeszcze kilka uwag dot. warsztatu - do tworzenia gry wystarcza w zupełności przeglądarka internetowa i notatnik - do czasu publikacji nie jest potrzebny serwer.

Jeżeli po uruchomieniu pliku html z grą coś nie działa, trzeba skorzystać z podglądu konsoli Javascript - w Firefox jest ona dostępna pod klawiszem ctrl+shift+k. Będą tam dostępne informacje n/t błędów Javascript.

Rozpoczęcie prac nad grą

Prace nad własną grą warto prowadzić w osobnym folderze - wiele gier w tym samym folderze ma dostęp do tych samych zapisów stanu gry, co prawdopodobnie spowodowałoby komplikacje.

Do folderu z projektem gry trzeba skopiować plik gamecore.js, oraz szablon.html.

Nazwę pliku szablon.html warto zmienić na inną, np. index.html.

Kod gry można wpisywać w pliku index.html w miejscu na javascript, albo też wyodrębnić go w osobnym pliku .js, który będzie podlinkowany w pliku index.html na takiej samej zasadzie jak plik gamecore.js.

W miarę rozwoju gry, może się okazać przydatne wyodrębnienie jej różnych części do wielu plików .js - wówczas wszystkie te pliki należy podlinkować w sekcji head pliku html.

Changelog

Wersja 2

funkcja opt: usunięty trzeci parametr optparam, obecnie można przekazać dowolną ilość parametrów do funkcji akcji:

for (var x=1; x<3; x++)
for (var y=1; y<3; y++)
opt('Opcja x='+x+', y='+y,
function(x,y)
{
p('x='+x+', y='+y);
},
x,y);