Interactive game on XSLT

Once upon a time, people came up with the XML language and saw that it was good. And they began to use it wherever possible, and even where it should not. Formats for data storage and transfer, configs, web services, databases ... It seemed to look around - XML, XML everywhere. Time passed, people changed their minds, invented various other data formats (or hid XML inside archives), and XML madness seemed to quiet down. But since then, almost any system has been able to XML and integrate such systems (who said Apache Camel?) Is the best and easiest way using XML documents.
Where XML is, there is XSLT, a language designed to transform XML documents. This language is specialized, but has the property of completeness according to Turing . Therefore, the language is suitable for "abnormal" use. Here, for example, existssolving the problem of 8 queens . So, you can write a game.
For the impatient: a working program on JSFiddle , sources on GitHub .
Any program converts input to output. Three parts can be distinguished in the program: preprocessor, processor, and postprocessor. The preprocessor prepares the input data for further processing. The processor is engaged in the main work of data conversion, if necessary, "mixing" user input and external signals and events, including in a cycle. A postprocessor is needed to convert the results of the processor into a form suitable for human perception (or by other programs).

In the case of an interactive game on XSLT, each of the three parts of the program will be a separate XSLT file. The preprocessor will prepare the playing field. The processor will apply the move of the human player, make the move of the computer player and determine the winner. The postprocessor will render the state of the game.
An XSLT program needs a runtime. The most common runtime capable of executing XSLT is any modern browser . We will use XSLT version 1.0, as it is supported by browsers out of the box.
A bit about XSLT and XPath
XSLT is an XML document translation language; XPath is used to access parts of an XML document. Specifications for these languages are published on w3.org: XSLT Version 1.0 and XPath Version 1.0 .
The basics and usage examples of XSLT and XPath are easily searched on the net. Here I will pay attention to the features that need to be considered when trying to use XSLT as a "normal" general-purpose high-level programming language.
XSLT has named functions. They are declared an element.
<xsl:templatename="function_name"/>
and are called this way:
<xsl:call-templatename="function_name"/>
Functions may have parameters.
Announcement:
<xsl:templatename="add"><xsl:paramname="x"/><xsl:paramname="y"/><xsl:value-ofselect="$x + $y"/></xsl:template>
Function call with parameters:
<xsl:call-templatename="add"><xsl:with-paramname="x"select="1"/><xsl:with-paramname="y"select="2"/></xsl:call-template>
Parameters can have default values.
Parameters can be "global" coming from outside. Using these parameters, user input will be transferred to the program.
The language also allows you to declare variables that may be associated with a value. Parameters and variables are immutable and values can be assigned to them once (just like in Erlang, for example).
XPath defines four basic data types: string, number, boolean, and node-set. XSLT adds a fifth type - a result tree fragment. This fragment looks like a node-set, but with it you can perform a limited set of operations. It can be copied entirely to the XML output document, but you cannot access the child nodes.
<xsl:variablename="board"><cell>1</cell><cell>2</cell><cell>3</cell><cell>4</cell></xsl:variable>
The board variable contains a fragment of the XML document. But the child nodes cannot be accessed. This code is not valid:
<xsl:for-eachselect="$board/cell"/>
The best you can get is access to the text nodes of the fragment and work with them as a string:
<xsl:value-ofselect="substring(string($board), 2, 1)"/>
will return "2".
Because of this, in our game, the board (or playing field) will be presented as a string so that it can be arbitrarily manipulated.
XSLT allows you to iterate a node-set using the xsl: for-each construct. But the language does not have the usual for or while loops. Instead, you can use a recursive function call (iteration and recursion are isomorphic). A loop of the form for x in a..b will be organized like this:
<xsl:call-templatename="for_loop"><xsl:with-paramname="x"select="$a"/><xsl:with-paramname="to"select="$b"/></xsl:call-template><xsl:templatename="for_loop"><xsl:paramname="x"/><xsl:paramname="to"/><xsl:iftest="$x < $to"><!-- сделать что-нибудь полезное --><xsl:call-templatename="for_loop"><xsl:with-paramname="x"select="$x + 1"/><xsl:with-paramname="to"select="$to"/></xsl:call-template></xsl:if></xsl:template>
Writing a runtime
The program requires: 3 XSLT, source XML, user input (parameters), internal state XML and output XML.
We place text fields with identifiers in the html-file : "preprocessor-xslt", "processor-xslt", "postprocessor-xslt", "input-xml", "parameters", "output-xml", "postprocessed-xml". We also place /> to embed the result in the page (for visualization).
Add two buttons: initialization and call (step) of the processor.
Let's write some JavaScript code.
A key feature is the use of XSLT transformation.
functiontransform(xslt, xml, params) {
var processor = new XSLTProcessor();
var parser = new DOMParser();
var xsltDom = parser.parseFromString(xslt, "application/xml");
// TODO: check errors .documentElement.nodeName == "parsererror"var xmlDom = parser.parseFromString(xml, "application/xml");
processor.importStylesheet(xsltDom);
if (typeof params !== 'undefined') {
params.forEach(function(value, key) {
processor.setParameter("", key, value);
});
}
var result = processor.transformToDocument(xmlDom);
var serializer = new XMLSerializer();
return serializer.serializeToString(result);
}
Functions of the preprocessor, processor and postprocessor:
functiondoPreprocessing() {
var xslt = document.getElementById("preprocessor-xslt").value;
var xml = document.getElementById("input-xml").value;
var result = transform(xslt, xml);
document.getElementById("output-xml").value = result;
}
functiondoProcessing() {
var params = parseParams(document.getElementById("parameters").value);
var xslt = document.getElementById("processor-xslt").value;
var xml = document.getElementById("output-xml").value;
var result = transform(xslt, xml, params);
document.getElementById("output-xml").value = result;
}
functiondoPostprocessing() {
var xslt = document.getElementById("postprocessor-xslt").value;
var xml = document.getElementById("output-xml").value;
var result = transform(xslt, xml);
document.getElementById("postprocessed-xml").value = result;
document.getElementById("output").innerHTML = result;
}
The helper function parseParams () parses user input into key = value pairs.
The initialization button calls up
functiononInit() {
doPreprocessing();
doPostprocessing();
}
Processor start button
functiononStep() {
doProcessing();
doPostprocessing();
}
Basic runtime is ready.
How to use it. Insert three XSLT documents into the appropriate fields. Insert an XML input document. Press the “Init” button. If necessary, enter the required values in the parameter field. Click the Step button.
Writing a game
If someone else hasn’t guessed, the interactive game from the title is the classic 3 by 3 tic-tac-toe.
The playing field is a 3 by 3 table, the cells of which are numbered from 1 to 9.
The human player always crosses (“X” symbol ), the computer - zeroes (“O”). If the cell is occupied by a cross or a zero, the corresponding digit is replaced by the symbol “X” or “O”.
The state of the game is contained in an XML document of this form:
<game><board>123456789</board><state></state><beginner></beginner><message></message></game>
The <board /> element contains the playing field; <state /> - the state of the game (winning one of the players or a draw or mistake); the <beginner /> element is used to determine who started the current game (so that another player starts the next game); <message /> - message for the player.
The preprocessor generates an initial state (empty field) from an arbitrary XML document.
The processor validates user input, applies its move, calculates the state of the game, calculates and applies the move of the computer.
On pseudo code, it looks something like this
fn do_move() {
let board_after_human_move = apply_move(board, "X", param)
let state_after_human_move = get_board_state(board_after_human_move)
if state_after_human_move = "" {
let board_after_computer_move = make_computer_move(board_after_human_move)
let state_after_computer_move = get_board_state(board_after_computer_move)
return (board_after_computer_move, state_after_computer_move)
} else {
return (board_after_human_move, state_after_human_move)
}
}
fn apply_move(board, player, index) {
// функция заменяет в строке board символ по индексу index на символ player и возвращающая новую строку
}
fn get_board_state(board) {
// функция возвращает "X", если выиграл человек, "O", если выиграл компьютер, "tie" в случае ничьей и пустую строку в остальных случаях
}
fn make_computer_move(board) {
let position = get_the_best_move(board)
return apply_move(board, "O", position)
}
fn get_the_best_move(board) {
return get_the_best_move_loop(board, 1, 1, -1000)
}
fn get_the_best_move_loop(board, index, position, score) {
if index > 9 {
return position
} else if cell_is_free(board, index) {
let new_board = apply_move(board, "O", index)
let new_score = minimax(new_board, "X", 0)
if score < new_score {
return get_the_best_move_loop(board, index + 1, index, new_score)
} else {
return get_the_best_move_loop(board, index + 1, position, score)
}
} else {
return get_the_best_move_loop(board, index + 1, position, score)
}
}
fn cell_is_free(board, index) {
// функция возвращает true, если в строке board по индексу index находится цифра (клетка свободна)
}
fn minimax(board, player, depth) {
let state = get_board_state(board)
if state = "X" {
// выиграл человек
return -10 + depth
} else if state = "O" {
// выиграл компьютер
return 10 - depth
} else if state = "tie" {
// ничья
return 0
} else {
let score = if player = "X" { 1000 } else { -1000 }
return minimax_loop(board, player, depth, 1, score)
}
}
fn minimax_loop(board, player, depth, index, score) {
if index > 9 {
return score
} else if cell_is_free(board, index) {
// если клетка свободна, вычисляем её оценку
let new_board = apply_move(board, player, index)
let new_score = minimax(new_board, switch_player(player), depth + 1)
let the_best_score = if player = "X" {
// человек минимизирует счёт
if new_score < score { new_score } else { score }
} else {
// компьютер максимизирует счёт
if new_score > score { new_score } else { score }
}
return minimax_loop(board, player, depth, index + 1, the_best_score)
} else {
// иначе переход на следующую клетку
return minimax_loop(board, player, depth, index + 1, score)
}
}
fn switch_player(player) {
// функция меняет игрока; X -> O, O -> X
}
The computer move selection function uses the minimax algorithm, where the computer maximizes its score, and the person minimizes it. The depth parameter of the minimax function is needed to select a move that leads to victory in the least number of moves.
This algorithm uses a large number of recursive calls and the first move of the computer is calculated on my machine up to 2-3 seconds. We must somehow accelerate. You can simply take and pre-calculate the best moves of the computer for all possible acceptable gambling field conditions. Such states turned out to be 886. It is possible to reduce this number due to rotations and reflections of the field, but it is not necessary. The new version is fast.
It's time to beautifully display the playing field. What to use if this is something a) should draw graphics (21st century in the yard, what kind of game without graphics ?!) and b) it is desirable to have an XML format? Of course SVG!
The postprocessor draws a checkered field and arranges green crosses, blue zeroes and small black
And it’s like the game is ready. But something is not right. To play, you need to make a lot of unnecessary, boring and annoying actions: enter the cell number in the field for the next code and press the button. Just click on the desired cell!
We are finalizing runtime and postprocessor .
In the runtime, add the response function of clicking on the SVG element:
functiononSvgClick(arg) {
document.getElementById("parameters").value = arg;
onStep();
}
In the postprocessor we add a square above each cell (transparency is specified by the rect.btn style) square, when clicked, a function with the cell number is called:
<rectclass="btn"x="-23"y="-23"width="45"height="45"onclick="onSvgClick({$index})"/>
After the batch is completed, clicking on any cell starts a new one. The “Init” button needs to be pressed only once at the very beginning.
Now you can consider the game finished. The point is small: hide the insides, pack it in an electron application, put it on Steam, ???, wake up rich and famous.
Conclusion
A strong-minded programmer can write anything on anything