the class "CodeMirror-activeline-background".
6 |
7 | (function(mod) {
8 | if (typeof exports == "object" && typeof module == "object") // CommonJS
9 | mod(require("../../lib/codemirror"));
10 | else if (typeof define == "function" && define.amd) // AMD
11 | define(["../../lib/codemirror"], mod);
12 | else // Plain browser env
13 | mod(CodeMirror);
14 | })(function(CodeMirror) {
15 | "use strict";
16 | var WRAP_CLASS = "CodeMirror-activeline";
17 | var BACK_CLASS = "CodeMirror-activeline-background";
18 |
19 | CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
20 | var prev = old && old != CodeMirror.Init;
21 | if (val && !prev) {
22 | cm.state.activeLines = [];
23 | updateActiveLines(cm, cm.listSelections());
24 | cm.on("beforeSelectionChange", selectionChange);
25 | } else if (!val && prev) {
26 | cm.off("beforeSelectionChange", selectionChange);
27 | clearActiveLines(cm);
28 | delete cm.state.activeLines;
29 | }
30 | });
31 |
32 | function clearActiveLines(cm) {
33 | for (var i = 0; i < cm.state.activeLines.length; i++) {
34 | cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
35 | cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
36 | }
37 | }
38 |
39 | function sameArray(a, b) {
40 | if (a.length != b.length) return false;
41 | for (var i = 0; i < a.length; i++)
42 | if (a[i] != b[i]) return false;
43 | return true;
44 | }
45 |
46 | function updateActiveLines(cm, ranges) {
47 | var active = [];
48 | for (var i = 0; i < ranges.length; i++) {
49 | var line = cm.getLineHandleVisualStart(ranges[i].head.line);
50 | if (active[active.length - 1] != line) active.push(line);
51 | }
52 | if (sameArray(cm.state.activeLines, active)) return;
53 | cm.operation(function() {
54 | clearActiveLines(cm);
55 | for (var i = 0; i < active.length; i++) {
56 | cm.addLineClass(active[i], "wrap", WRAP_CLASS);
57 | cm.addLineClass(active[i], "background", BACK_CLASS);
58 | }
59 | cm.state.activeLines = active;
60 | });
61 | }
62 |
63 | function selectionChange(cm, sel) {
64 | updateActiveLines(cm, sel.ranges);
65 | }
66 | });
67 |
--------------------------------------------------------------------------------
/src/js/codemirror/match-highlighter.js:
--------------------------------------------------------------------------------
1 | // Highlighting text that matches the selection
2 | //
3 | // Defines an option highlightSelectionMatches, which, when enabled,
4 | // will style strings that match the selection throughout the
5 | // document.
6 | //
7 | // The option can be set to true to simply enable it, or to a
8 | // {minChars, style, showToken} object to explicitly configure it.
9 | // minChars is the minimum amount of characters that should be
10 | // selected for the behavior to occur, and style is the token style to
11 | // apply to the matches. This will be prefixed by "cm-" to create an
12 | // actual CSS class name. showToken, when enabled, will cause the
13 | // current token to be highlighted when nothing is selected.
14 |
15 | (function(mod) {
16 | if (typeof exports == "object" && typeof module == "object") // CommonJS
17 | mod(require("../../lib/codemirror"));
18 | else if (typeof define == "function" && define.amd) // AMD
19 | define(["../../lib/codemirror"], mod);
20 | else // Plain browser env
21 | mod(CodeMirror);
22 | })(function(CodeMirror) {
23 | "use strict";
24 |
25 | var DEFAULT_MIN_CHARS = 2;
26 | var DEFAULT_TOKEN_STYLE = "matchhighlight";
27 | var DEFAULT_DELAY = 100;
28 |
29 | function State(options) {
30 | if (typeof options == "object") {
31 | this.minChars = options.minChars;
32 | this.style = options.style;
33 | this.showToken = options.showToken;
34 | this.delay = options.delay;
35 | }
36 | if (this.style == null) this.style = DEFAULT_TOKEN_STYLE;
37 | if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS;
38 | if (this.delay == null) this.delay = DEFAULT_DELAY;
39 | this.overlay = this.timeout = null;
40 | }
41 |
42 | CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
43 | if (old && old != CodeMirror.Init) {
44 | var over = cm.state.matchHighlighter.overlay;
45 | if (over) cm.removeOverlay(over);
46 | clearTimeout(cm.state.matchHighlighter.timeout);
47 | cm.state.matchHighlighter = null;
48 | cm.off("cursorActivity", cursorActivity);
49 | }
50 | if (val) {
51 | cm.state.matchHighlighter = new State(val);
52 | highlightMatches(cm);
53 | cm.on("cursorActivity", cursorActivity);
54 | }
55 | });
56 |
57 | function cursorActivity(cm) {
58 | var state = cm.state.matchHighlighter;
59 | clearTimeout(state.timeout);
60 | state.timeout = setTimeout(function() {highlightMatches(cm);}, state.delay);
61 | }
62 |
63 | function highlightMatches(cm) {
64 | cm.operation(function() {
65 | var state = cm.state.matchHighlighter;
66 | if (state.overlay) {
67 | cm.removeOverlay(state.overlay);
68 | state.overlay = null;
69 | }
70 | if (!cm.somethingSelected() && state.showToken) {
71 | var re = state.showToken === true ? /[\w$]/ : state.showToken;
72 | var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
73 | while (start && re.test(line.charAt(start - 1))) --start;
74 | while (end < line.length && re.test(line.charAt(end))) ++end;
75 | if (start < end)
76 | cm.addOverlay(state.overlay = makeOverlay(line.slice(start, end), re, state.style));
77 | return;
78 | }
79 | if (cm.getCursor("head").line != cm.getCursor("anchor").line) return;
80 | var selection = cm.getSelections()[0].replace(/^\s+|\s+$/g, "");
81 | if (selection.length >= state.minChars)
82 | cm.addOverlay(state.overlay = makeOverlay(selection, false, state.style));
83 | });
84 | }
85 |
86 | function boundariesAround(stream, re) {
87 | return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
88 | (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
89 | }
90 |
91 | function makeOverlay(query, hasBoundary, style) {
92 | return {token: function(stream) {
93 | if (stream.match(query) &&
94 | (!hasBoundary || boundariesAround(stream, hasBoundary)))
95 | return style;
96 | stream.next();
97 | stream.skipTo(query.charAt(0)) || stream.skipToEnd();
98 | }};
99 | }
100 | });
101 |
--------------------------------------------------------------------------------
/src/js/codemirror/stringstream.js:
--------------------------------------------------------------------------------
1 | /*
2 | Code extracted from codemirror/codemirror.js that is used for parsing and is the only reason why PuzzleScript player had to include codemirror.
3 | */
4 |
5 | function CodeMirrorStringStream(string, tabSize)
6 | {
7 | this.pos = this.start = 0;
8 | this.string = string;
9 | this.tabSize = tabSize || 8;
10 | this.lastColumnPos = this.lastColumnValue = 0;
11 | this.lineStart = 0;
12 | }
13 |
14 | CodeMirrorStringStream.prototype = {
15 | eol: function() {return this.pos >= this.string.length},
16 | sol: function() {return this.pos == this.lineStart},
17 | peek: function() {return this.string.charAt(this.pos) || undefined},
18 | next: function()
19 | {
20 | if (this.pos < this.string.length)
21 | return this.string.charAt(this.pos++)
22 | },
23 | eat: function(match)
24 | {
25 | var ch = this.string.charAt(this.pos)
26 | if (typeof match == "string")
27 | var ok = ch == match
28 | else
29 | var ok = ch && (match.test ? match.test(ch) : match(ch));
30 | if (ok)
31 | {
32 | ++this.pos;
33 | return ch;
34 | }
35 | },
36 | eatWhile: function(match)
37 | {
38 | var start = this.pos
39 | while (this.eat(match)) { }
40 | return this.pos > start
41 | },
42 | skipToEnd: function() { this.pos = this.string.length },
43 | skipTo: function(ch)
44 | {
45 | const found = this.string.indexOf(ch, this.pos)
46 | if (found > -1)
47 | {
48 | this.pos = found
49 | return true
50 | }
51 | },
52 | match: function(pattern, consume, caseInsensitive)
53 | {
54 | if (typeof pattern == "string")
55 | {
56 | var cased = (str) => (caseInsensitive ? str.toLowerCase() : str)
57 | var substr = this.string.substr(this.pos, pattern.length)
58 | if (cased(substr) == cased(pattern))
59 | {
60 | if (consume !== false)
61 | this.pos += pattern.length
62 | return true
63 | }
64 | }
65 | else
66 | {
67 | var match = this.string.slice(this.pos).match(pattern)
68 | if (match && match.index > 0) return null
69 | if (match && consume !== false)
70 | this.pos += match[0].length
71 | return match
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/js/compiler/rule.js:
--------------------------------------------------------------------------------
1 |
2 | // ======= CLONING RULES =======
3 |
4 | function deepCloneCellRow(cellrow)
5 | {
6 | return cellrow.map(
7 | cell => cell.map( oc => (oc === null) ? null : ({dir: oc.dir, ii: oc.ii, no: oc.no, random: oc.random}) )
8 | );
9 | }
10 |
11 | function deepCloneHS(HS)
12 | {
13 | return HS.map( deepCloneCellRow );
14 | }
15 |
16 | function deepCloneRule(rule)
17 | {
18 | return {
19 | lineNumber: rule.lineNumber,
20 | groupNumber: rule.groupNumber,
21 | direction: rule.direction,
22 | tag_classes: rule.tag_classes,
23 | tag_classes_replacements: rule.tag_classes_replacements,
24 | parameter_properties: rule.parameter_properties,
25 | parameter_properties_replacements: rule.parameter_properties_replacements,
26 | parameter_expansion_string: rule.parameter_expansion_string,
27 | late: rule.late,
28 | rigid: rule.rigid,
29 | randomRule:rule.randomRule,
30 | lhs: deepCloneHS(rule.lhs),
31 | rhs: deepCloneHS(rule.rhs),
32 | commands: rule.commands, // should be deepCloned too?
33 | is_directional: rule.is_directional
34 | };
35 | }
36 |
37 |
38 |
39 | // ======= PRINTING RULES =======
40 |
41 | function printCell(identifiers, cell)
42 | {
43 | var result = '';
44 | for (const oc of cell)
45 | {
46 | if (oc === null)
47 | {
48 | result += '... '
49 | }
50 | else
51 | {
52 | if (oc.no)
53 | result += 'no '
54 | if (oc.random)
55 | result += 'random '
56 | result += oc.dir + ' '
57 | result += identifiers.names[oc.ii]+' '
58 | }
59 | }
60 | return result
61 | }
62 |
63 | function printCellRow(identifiers, cellRow)
64 | {
65 | return '[ ' + cellRow.map(c => printCell(identifiers,c)).join('| ') + '] ';
66 | }
67 |
68 | function cacheRuleStringRep(identifiers, rule)
69 | {
70 | var result='('+makeLinkToLine(rule.lineNumber)+') '+ rule.direction.toString().toUpperCase()+ ' ';
71 | if (rule.tag_classes.size > 0)
72 | {
73 | result += [...rule.tag_classes].map( (tc_ii, i) => (identifiers.names[tc_ii].toUpperCase()+'='+identifiers.names[rule.tag_classes_replacements[i]]) ).join(', ') + ' '
74 | }
75 | if (rule.parameter_properties.size > 0)
76 | {
77 | result += [...rule.parameter_properties].map( (pp_ii, i) => (identifiers.names[pp_ii].toUpperCase()+'='+identifiers.names[rule.parameter_properties_replacements[i]]) ).join(', ') + ' '
78 | }
79 | if (rule.rigid) {
80 | result = "RIGID "+result+" ";
81 | }
82 | if (rule.randomRule) {
83 | result = "RANDOM "+result+" ";
84 | }
85 | if (rule.late) {
86 | result = "LATE "+result+" ";
87 | }
88 | for (const cellRow of rule.lhs) {
89 | result = result + printCellRow(identifiers, cellRow);
90 | }
91 | result = result + "-> ";
92 | for (const cellRow of rule.rhs) {
93 | result = result + printCellRow(identifiers, cellRow);
94 | }
95 | result += rule.commands.get_representation()
96 | rule.stringRep = result
97 | }
98 |
99 | function cacheAllRuleNames(state)
100 | {
101 | for (const rule of state.rules)
102 | {
103 | cacheRuleStringRep(state.identifiers, rule);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/js/debug.js:
--------------------------------------------------------------------------------
1 | var canSetHTMLColors=false;
2 | var canDump=true;
3 | var canYoutube=false;
4 | var inputHistory=[];
5 | var compiledText;
6 | var IDE=true;
7 | var recordingStartsFromLevel = 0 // for input recorder
8 |
9 | Level.prototype.convertToString = function(def_char = '=')
10 | {
11 | var out = ''
12 | var seenCells = {}
13 | var i = 0
14 | for (var y = 0; y < this.height; y++)
15 | {
16 | for (var x = 0; x < this.width; x++)
17 | {
18 | const bitmask = this.getCell(x + y*this.width) // TODO: it should be y + x*this.height
19 | var objs = [];
20 | for (var bit = 0; bit < 32 * STRIDE_OBJ; ++bit)
21 | {
22 | if (bitmask.get(bit))
23 | {
24 | objs.push(state.identifiers.objects[state.idDict[bit]].name)
25 | }
26 | }
27 | objs.sort()
28 | objs = objs.join(' ')
29 | /* replace repeated object combinations with numbers */
30 | if (!seenCells.hasOwnProperty(objs))
31 | {
32 | seenCells[objs] = i++
33 | out += objs + def_char
34 | }
35 | out += seenCells[objs] + ','
36 | }
37 | out += '\n'
38 | }
39 | return out
40 | }
41 |
42 | function levelFromUnitTestString(str)
43 | {
44 | const lines = str.split('\n')
45 | const height = lines.length - 1
46 | const width = lines[0].split(',').length - 1
47 | var lev = new Level(width, height, new Int32Array(width * height * STRIDE_OBJ))
48 | execution_context.resetCommands()
49 | var masks = []
50 | for (const [y, line] of lines.entries())
51 | {
52 | for (const [x, cell_content] of line.split(',').entries())
53 | {
54 | if (cell_content.length == 0)
55 | continue
56 | var cell_parts = cell_content.split(/[:=]/) // : is used by vanilla PuzzleScript but appears in names of objects with tags so it is replaced with = in Pattern:Script.
57 | if (cell_parts.length > 1)
58 | {
59 | const object_names = cell_parts[0].split(' ')
60 | const objects = object_names.map( object_name => state.identifiers.objects.find( o => (object_name === o.name) ) )
61 | const mask = makeMaskFromGlyph( objects.map( o => o.id ) )
62 | masks.push(mask)
63 | }
64 | const mask_id = parseInt(cell_parts[cell_parts.length - 1])
65 | const maskint = masks[mask_id]
66 | lev.setCell(x + y*width, maskint)
67 | }
68 | }
69 | return lev
70 | }
71 |
72 | function loadUnitTestStringLevel(str)
73 | {
74 | loadLevelFromLevelDat(state, levelFromUnitTestString(str), null)
75 | }
76 |
77 |
78 | function stripHTMLTags(html_str)
79 | {
80 | if (typeof html_str !== 'string')
81 | return html_str
82 | var div = document.createElement("div");
83 | div.innerHTML = html_str;
84 | var text = div.textContent || div.innerText || "";
85 | return text.trim();
86 | }
87 |
88 | function dumpTestCase()
89 | {
90 | //compiler error data
91 | const levelDat = compiledText
92 | const resultstring = '\t[
\t\t`' + (state.metadata.title||'untitled test') + '`,
\t\t' +
93 | JSON.stringify( [levelDat, errorStrings.map(stripHTMLTags), warningStrings.map(stripHTMLTags)] ) + '
\t],
'
94 | consolePrint('
Compilation error/warning data (for error message tests - errormessage_testdata.js):
' + makeSelectableText(resultstring) + '
', true)
95 |
96 | //normal session recording data
97 | if (level !== undefined)
98 | {
99 | const resultstring = '\t[
\t\t`' + (state.metadata.title||'untitled test') + '`,
\t\t' +
100 | JSON.stringify( [levelDat, inputHistory.concat([]), level.convertToString(), recordingStartsFromLevel, RandomGen.seed] ) + '
\t],
'
101 | consolePrint('
Recorded play session data (for play session tests - testdata.js):
'+makeSelectableText(resultstring) + '
', true)
102 | }
103 | }
104 |
105 | function clearInputHistory()
106 | {
107 | if (canDump === true)
108 | {
109 | inputHistory=[]
110 | recordingStartsFromLevel = curlevel
111 | }
112 | }
113 |
114 | function pushInput(inp) {
115 | if (canDump===true) {
116 | inputHistory.push(inp);
117 | }
118 | }
119 |
120 |
121 | function print_ruleset(rule_set)
122 | {
123 | var output = ''
124 | for (const rulegroup of rule_set)
125 | {
126 | output += ' ' + rulegroup.map(rule => rule.string_representation).join('
+ ') + '
'
127 | }
128 | return output
129 | }
130 |
--------------------------------------------------------------------------------
/src/js/debug_off.js:
--------------------------------------------------------------------------------
1 | // from console.js
2 |
3 | function addErrorMessage(str)
4 | {
5 | var errorText = document.getElementById("errormessage")
6 |
7 | var div = document.createElement("div");
8 | div.innerHTML = str;
9 | str = div.textContent || div.innerText || "";
10 |
11 | errorText.innerHTML += str + "
"
12 | }
13 |
14 | function consolePrint(str, urgent) { /* addErrorMessage(str) */ }
15 |
16 | function consolePrintFromRule(str, rule, urgent) { /* addErrorMessage(str) */ }
17 |
18 | function consoleCacheDump(str) { }
19 |
20 | function consoleError(str, lineNumber) { addErrorMessage(str) }
21 |
22 |
23 |
24 | // from debug.js
25 |
26 | var canSetHTMLColors=true;
27 | var canDump=false;
28 | var canYoutube=true;
29 | var IDE=false;
30 |
31 | function clearInputHistory() { }
32 | function pushInput(inp) { }
--------------------------------------------------------------------------------
/src/js/engine/commands_set.js:
--------------------------------------------------------------------------------
1 |
2 | function CommandsSet()
3 | {
4 | BitVec.call(this, 13) // todo
5 | this.message = null
6 | this.nb_commands = 0
7 | }
8 | CommandsSet.prototype = Object.create(BitVec.prototype)
9 |
10 | // DO NOT CHANGE THE ORDER OF COMMANDS
11 | CommandsSet.commandwords = [ 'cancel', 'restart', 'again', 'win', 'checkpoint', 'nosave', 'sfx0', 'sfx1', 'sfx2', 'sfx3', 'sfx4', 'sfx5', 'sfx6', 'sfx7', 'sfx8', 'sfx9', 'sfx10', 'message' ]
12 | CommandsSet.command_keys = {}
13 | CommandsSet.commandwords.forEach( (word, index) => { CommandsSet.command_keys[word] = index} )
14 |
15 | CommandsSet.prototype.is_command = function(word)
16 | {
17 | return CommandsSet.command_keys.hasOwnProperty(word)
18 | }
19 |
20 | CommandsSet.prototype.addCommand = function(command)
21 | {
22 | const key = CommandsSet.command_keys[command]
23 | if (this.get(key))
24 | return
25 | this.ibitset(key)
26 | this.nb_commands++
27 | }
28 |
29 | CommandsSet.prototype.setMessage = function(msg_text)
30 | {
31 | this.message = msg_text
32 | this.addCommand('message')
33 | }
34 |
35 | CommandsSet.prototype.reset = function()
36 | {
37 | this.setZero()
38 | this.message = null
39 | }
40 |
41 | CommandsSet.prototype.get_representation = function()
42 | {
43 | return CommandsSet.commandwords.filter( (k,i) => this.get(i) ).join(' ').replace('message', '(message, "'+this.message+'")')
44 | }
--------------------------------------------------------------------------------
/src/js/engine/generate_matches.js:
--------------------------------------------------------------------------------
1 |
2 | /* THIS FILE IS NOT FOR EXECUTION !!!!
3 | * it is for documenting this complex part of the engine that is the generation of specific rule-matching functions
4 | */
5 |
6 |
7 | // Cells
8 | // =====
9 |
10 | // The generated functions are a specialized case of:
11 | function matchCell()
12 | {
13 | return (
14 | ((level.objects[i] & cell_pattern.objectsPresent) == cell_pattern.objectsPresent)
15 | && ((level.objects[i] & cell_pattern.objectsMissing) == 0)
16 | && ((level.movements[i] & cell_pattern.movementsPresent) == cell_pattern.movementsPresent)
17 | && ((level.movements[i] & cell_pattern.movementsMissing) == 0)
18 | && cell_pattern.anyObjectsPresent.every( s => ((level.objects[i] & s) != 0) )
19 | )
20 | }
21 | // - Note that there is no anyMovementPresent, because the objects in an anyObjectsPresent slot are all in the same layer and thus share the same movement, which has been
22 | // put in movementsPresent / movementsMissing.
23 | // - Also, there is no anyObjectMissing, because that would correspond to object conditions like 'no single-layer-property', which has been translated into 'no obj1 no obj2...'
24 | // and put in objectsMissing.
25 | // - The case of 'no aggregate' = 'no obj1 or no obj2 or...' could be dealt easily with a test like:
26 | // ((level.objects[i] & cell_pattern.aggregateMissing) !== cell_pattern.aggregateMissing)
27 |
28 | // Cell Rows
29 | // =========
30 |
31 | // This function has been replaced with a direct call to Rule.cellRowMatches[cellRowIndex](this.patterns[cellRowIndex], tuple[cellRowIndex], delta_index),
32 | // i.e. a direct call to the function generated by Rule.generateCellRowMatchesFunction.
33 | function DoesCellRowMatch(delta_index, cellRow, start_cell_index)
34 | {
35 | var targetIndex = start_cell_index
36 | for (const cellPattern of cellRow)
37 | {
38 | if ( ! cellPattern.matches(targetIndex) )
39 | return false
40 | targetIndex += delta_index
41 | }
42 | return true
43 | }
44 |
45 | // Say cellRow has length 3
46 | // CellRow Matches can be specialized to look something like:
47 | function cellRowMatchesFunctionGenerate(delta_index, cellRow, i)
48 | {
49 | return cellRow[0].matches(i) && cellRow[1].matches(i+delta_index) && cellRow[2].matches(i+2*delta_index)
50 | }
51 |
52 | function DoesCellRowMatchWildCard(delta_index, cellRow, start_cell_index, maxk, mink=0)
53 | {
54 | var targetIndex = start_cell_index
55 |
56 | for (var j=0; j
= cellRow.length)
75 | return true
76 | }
77 | break
78 | }
79 | else if (!cellPattern.matches(targetIndex))
80 | break
81 | targetIndex += delta_index
82 | }
83 | return false
84 | }
85 |
86 | // Say cellRow has length 3, with a split in the middle
87 | // CellRow Matches WildCards can be specialized to look something like:
88 | function cellRowMatchesWildcardFunctionGenerate(delta_index, cellRow,i, maxk, mink)
89 | {
90 | var result = []
91 | var matchfirsthalf = cellRow[0].matches(i)
92 | if (matchfirsthalf)
93 | {
94 | for (var k=mink; k' + ((anchor_text === null) ? l : anchor_text) + '';
15 | }
16 |
17 | function logErrorCacheable(str, lineNumber, urgent)
18 | {
19 | logErrorAux(str, lineNumber, urgent, 'errorText', errorStrings)
20 | }
21 |
22 | function logError(str, lineNumber, urgent)
23 | {
24 | logErrorAux(str, lineNumber, urgent, 'errorText', errorStrings, true)
25 | }
26 |
27 | function logWarning(str, lineNumber, urgent)
28 | {
29 | logErrorAux(str, lineNumber, urgent, 'warningText', warningStrings, true)
30 | }
31 |
32 | function logErrorAux(str, lineNumber, urgent = false, text_class, text_cache, print_immediately)
33 | {
34 | if (compiling||urgent)
35 | {
36 | const txt = get_error_message(str)
37 | const lineString = (lineNumber !== undefined) ? makeLinkToLine(lineNumber, ' line ' + lineNumber.toString() + ' ') + ': ' : ''
38 | const errorString = lineString + '' + txt + ' '
39 | const key = (typeof str === 'string') ? str : [str, lineNumber]
40 | if (text_cache.findIndex(x => error_message_equal(x, key)) < 0 || urgent)
41 | {
42 | // not a duplicate error, we need to log it
43 | consolePrint(errorString, print_immediately)
44 | text_cache.push(key)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/js/font.js:
--------------------------------------------------------------------------------
1 | const font_width = 5
2 | const font_height = 12
3 |
4 | const chars_in_font = '0123456789abcdefghijklmnopqrstuvwx×yzABCDEFGHIJKLMNOPQRSTUVWXYZ.·•…†‡ƒ‚„,;:?¿!¡@£$%‰^&*()+÷±-–—_= {}[]\'‘’“”"/\\|¦<‹«>›»~˜`#' +
5 | 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßẞàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘř' +
6 | 'ŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽž€™¢¤¥§¨©®ªº¬¯°'
7 |
8 | var font = new Image()
9 | // <-- FONT START -->
10 | // <-- Do not edit these comments, they are used by buildStandalone.js to replace the loading of the font image with an inline version of the image. -->
11 | // <-- Note that ideally, we should keep track of all colors/chars used in the game and integrate only these in the inlined font picture. -->
12 | font.src = 'fonts/font-5x12.png'
13 |
14 | font.asDataURL = function()
15 | {
16 | var canvas = document.createElement('canvas')
17 | canvas.width = this.width
18 | canvas.height = this.height
19 | canvas.getContext('2d').drawImage(this, 0, 0)
20 | return canvas.toDataURL("image/png")
21 | }
22 | // <-- FONT END -->
23 | font.on_load = function() {}
24 | font.addEventListener('load', function()
25 | {
26 | const canvas = document.createElement('canvas')
27 | canvas.width = font.width
28 | canvas.height = font.height
29 | const fctx = canvas.getContext('2d')
30 | fctx.drawImage(font, 0, 0)
31 | font.pixels = fctx.getImageData(0, 0, canvas.width, canvas.height).data
32 | font.on_load()
33 | })
34 |
35 | font.colored_fonts = { '1-#FFFFFFFF': font }
36 |
37 | font.colored_font = function(css_color)
38 | {
39 | const key = css_color
40 | if (key in this.colored_fonts)
41 | return this.colored_fonts[key]
42 |
43 | if (font.pixels === undefined) // image is not loaded yet
44 | return null
45 |
46 | const color = Array.from( [1,3,5], i => parseInt(css_color.substr(i,2), 16) )
47 | const f_alpha = parseInt(css_color.substr(7,2), 16) || 255
48 |
49 | var canvas = document.createElement('canvas')
50 | canvas.width = this.width
51 | canvas.height = this.height
52 | var fctx = canvas.getContext('2d')
53 |
54 | for (var i = 0; i < this.pixels.length; i += 4)
55 | {
56 | const alpha = this.pixels[i+3]/255 // alpha channel. 0=transparent, 255=opaque
57 | if (alpha === 0)
58 | continue
59 | fctx.fillStyle = 'rgba(' + color.map(x => Math.round(x*alpha)).join() + ',' + f_alpha + ')'
60 | fctx.fillRect( ((i/4) % this.width), Math.floor((i/4) / this.width), 1, 1)
61 | }
62 | this.colored_fonts[key] = canvas
63 | return canvas
64 | }
65 |
66 | function draw_char(ctx, colored_font_image, ch, x, y, w, h) // draws char ch at position (x,y) in the canvas ctx with width w and height h
67 | {
68 | const ch_index = chars_in_font.indexOf(ch)
69 | if (ch_index < 0)
70 | return
71 | ctx.drawImage(colored_font_image, ch_index*w, 0, w, h, x, y, w, h)
72 | }
73 |
--------------------------------------------------------------------------------
/src/js/gamedat.js:
--------------------------------------------------------------------------------
1 | //var sourceCode = "title orthogonal rule test\n\n========\nOBJECTS\n========\n\nBackground .\nLIGHTGREEN GREEN\n11111\n01111\n11101\n11111\n10111\n\nPlayer p\nPink\n\nDiamond \nBlue\n.....\n.....\n..0..\n.....\n.....\n\nCrate\nOrange\n00000\n0...0\n0...0\n0...0\n00000\n\n=======\nLEGEND\n=======\n=======\nSOUNDS\n=======\n================\nCOLLISIONLAYERS\n================\n\nBackground\nPlayer\nCrate\nDiamond\n\n======\nRULES\n======\n\n[ ] -> [ Diamond ]\n\n==============\nWINCONDITIONS\n==============\n=======\nLEVELS\n=======\n\n.....\n.....\n..p..\n.....\n.....";
2 | var sourceCode = __GAMEDAT__;
3 | compile(["restart"],sourceCode);
4 |
--------------------------------------------------------------------------------
/src/js/globalVariables.js:
--------------------------------------------------------------------------------
1 | var unitTesting=false;
2 | var curlevel=0;
3 | var muted=0;
4 |
5 | const storage_get = (key) => localStorage.getItem(key)
6 | const storage_has = (key) => (localStorage.getItem(key) !== null)
7 | const storage_set = (key, value) => localStorage.setItem(key, value)
8 | const storage_remove = (key) => localStorage.removeItem(key)
9 |
10 | var debug = false
11 | var verbose_logging=false;
12 | var throttle_movement=false;
13 | var cache_console_messages=false;
14 | const deltatime = 17
15 | var timer=0;
16 | var repeatinterval=150;
17 | var autotick=0;
18 | var autotickinterval=0;
19 | var winning=false;
20 | var againing=false;
21 | var againinterval=150;
22 | var norepeat_action=false;
23 | var oldflickscreendat=[];//used for buffering old flickscreen/scrollscreen positions, in case player vanishes
24 | var keybuffer = []
25 |
26 | var level
27 |
28 | var sprite_width = 5
29 | var sprite_height = 5
30 |
31 | function clamp(min, value, max)
32 | {
33 | return (value < max) ? ( (value < min) ? min : value ) : max
34 | }
--------------------------------------------------------------------------------
/src/js/makegif.js:
--------------------------------------------------------------------------------
1 | function makeGIF()
2 | {
3 | var randomseed = RandomGen.seed;
4 | const inputDat = Array.from(inputHistory)
5 |
6 | unitTesting=true;
7 | levelString=compiledText;
8 |
9 | var encoder = new GIFEncoder();
10 | encoder.setRepeat(0); //auto-loop
11 | encoder.setDelay(200);
12 | encoder.start();
13 |
14 | // TODO: we should not have to recompile. Actually, we don't want to recompile. We want to use the replay the inputHistory in the same state (already compiled) than
15 | // when we created the inputHistory.
16 | compile(curlevel, levelString, randomseed)
17 |
18 | var gifcanvas = document.createElement('canvas');
19 | const [virtual_screen_w, virtual_screen_h] = screen_layout.content.get_virtual_screen_size()
20 | gifcanvas.width = gifcanvas.style.width = virtual_screen_w * screen_layout.magnification
21 | gifcanvas.height = gifcanvas.style.height = virtual_screen_h * screen_layout.magnification
22 | var gifctx = gifcanvas.getContext('2d');
23 |
24 | gifctx.drawImage(screen_layout.canvas, -screen_layout.margins[0], -screen_layout.margins[1]);
25 | encoder.addFrame(gifctx);
26 | var autotimer=0;
27 |
28 | for(const val of inputDat)
29 | {
30 | var realtimeframe = false
31 | switch (val)
32 | {
33 | case 'undo':
34 | execution_context.doUndo()
35 | break
36 | case 'restart':
37 | DoRestart()
38 | break
39 | case 'tick':
40 | processInput(processing_causes.autotick)
41 | realtimeframe = true
42 | break
43 | default:
44 | processInput(val)
45 | }
46 |
47 | redraw()
48 | gifctx.drawImage(screen_layout.canvas, -screen_layout.margins[0], -screen_layout.margins[1])
49 | encoder.addFrame(gifctx)
50 | encoder.setDelay(realtimeframe ? autotickinterval : repeatinterval)
51 | autotimer += repeatinterval
52 |
53 | while (againing)
54 | {
55 | processInput(processing_causes.again_frame)
56 | redraw()
57 |
58 | encoder.setDelay(againinterval)
59 | gifctx.drawImage(screen_layout.canvas, -screen_layout.margins[0], -screen_layout.margins[1])
60 | encoder.addFrame(gifctx)
61 | }
62 | }
63 |
64 | encoder.finish();
65 | const data_url = 'data:image/gif;base64,'+btoa(encoder.stream().getData());
66 | consolePrint(' ');
67 | consolePrint('Download GIF ');
68 |
69 | inputHistory = inputDat
70 | unitTesting = false
71 | }
72 |
--------------------------------------------------------------------------------
/src/js/rng.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Seedable random number generator functions.
3 | * @version 1.0.0
4 | * @license Public Domain
5 | *
6 | * @example
7 | * var rng = new RNG('Example');
8 | * rng.random(40, 50); // => 42
9 | * rng.uniform(); // => 0.7972798995050903
10 | */
11 |
12 | /**
13 | * Get the underlying bytes of this string.
14 | * @return {Array} An array of bytes
15 | */
16 | String.prototype.getBytes = function() {
17 | var output = [];
18 | for (var i = 0; i < this.length; i++) {
19 | var c = this.charCodeAt(i);
20 | var bytes = [];
21 | do {
22 | bytes.push(c & 0xFF);
23 | c = c >> 8;
24 | } while (c > 0);
25 | output = output.concat(bytes.reverse());
26 | }
27 | return output;
28 | }
29 |
30 | /**
31 | * @param {String} seed A string to seed the generator.
32 | * @constructor
33 | */
34 | function RC4(seed) {
35 | this.s = new Array(256);
36 | this.i = 0;
37 | this.j = 0;
38 | for (var i = 0; i < 256; i++) {
39 | this.s[i] = i;
40 | }
41 | if (seed) {
42 | this.mix(seed);
43 | }
44 | }
45 |
46 | RC4.prototype._swap = function(i, j) {
47 | var tmp = this.s[i];
48 | this.s[i] = this.s[j];
49 | this.s[j] = tmp;
50 | }
51 |
52 | /**
53 | * Mix additional entropy into this generator.
54 | * @param {String} seed
55 | */
56 | RC4.prototype.mix = function(seed)
57 | {
58 | const input = seed.getBytes()
59 | var j = 0;
60 | for (var i = 0; i < this.s.length; i++) {
61 | j += this.s[i] + input[i % input.length];
62 | j %= 256;
63 | this._swap(i, j);
64 | }
65 | }
66 |
67 | /**
68 | * @return {number} The next byte of output from the generator.
69 | */
70 | RC4.prototype.next = function() {
71 | this.i = (this.i + 1) % 256;
72 | this.j = (this.j + this.s[this.i]) % 256;
73 | this._swap(this.i, this.j);
74 | return this.s[(this.s[this.i] + this.s[this.j]) % 256];
75 | }
76 |
77 | /**
78 | * Create a new random number generator with optional seed. If the
79 | * provided seed is a function (i.e. Math.random) it will be used as
80 | * the uniform number generator.
81 | * @param seed An arbitrary object used to seed the generator.
82 | * @constructor
83 | */
84 | function RNG(seed)
85 | {
86 | if (seed == null) {
87 | seed = (Math.random() + Date.now()).toString()
88 | }
89 | else if (Object.prototype.toString.call(seed) !== '[object String]') {
90 | seed = JSON.stringify(seed);
91 | }
92 | this.seed = seed
93 | this._state = seed ? new RC4(seed) : null
94 | }
95 |
96 | /**
97 | * @return {number} Uniform random number between 0 and 255.
98 | */
99 | RNG.prototype.nextByte = function() {
100 | return this._state.next();
101 | }
102 |
103 | /**
104 | * @return {number} Uniform random number between 0 and 1.
105 | */
106 | RNG.prototype.uniform = function() {
107 | const BYTES = 7; // 56 bits to make a 53-bit double
108 | var output = 0;
109 | for (var i = 0; i < BYTES; i++) {
110 | output *= 256;
111 | output += this.nextByte();
112 | }
113 | return output / (Math.pow(2, BYTES * 8) - 1);
114 | }
115 |
116 | // Returns an integer in [0,max[
117 | RNG.prototype.integer = function(max) { return Math.floor(max*this.uniform()) }
118 |
119 | // Returns a random element of an array
120 | RNG.prototype.pickInArray = function(arr) { return arr[this.integer(arr.length)] }
121 |
--------------------------------------------------------------------------------
/src/js/soundbar.js:
--------------------------------------------------------------------------------
1 |
2 | var audio;
3 |
4 |
5 | function newSound(instrument)
6 | {
7 | const seed = instrument + 100 * ((Math.random() * 1000000) | 0)
8 | consolePrint(generatorNames[instrument] + ' : ' + '' + seed.toString() + ' ',true);
9 | var params = generateFromSeed(seed);
10 | params.sample_rate = SAMPLE_RATE;
11 | params.bit_depth = BIT_DEPTH;
12 | var sound = SoundEffect.generate(params);
13 | sound.play();
14 | }
15 |
16 | function buttonPress() {
17 | var generatortype = 3;
18 | var seed = document.getElementById('sounddat').value;
19 | var params = generateFromSeed(seed);
20 | params.sample_rate = SAMPLE_RATE;
21 | params.bit_depth = BIT_DEPTH;
22 | var sound = SoundEffect.generate(params);
23 | sound.play();
24 | }
25 |
--------------------------------------------------------------------------------
/src/runserver:
--------------------------------------------------------------------------------
1 | cd ../
2 | python -m SimpleHTTPServer 1234
3 |
--------------------------------------------------------------------------------
/src/tests/demo_list.txt:
--------------------------------------------------------------------------------
1 | I'm too far gone.txt
2 | Loop Deleter.txt
3 | README
4 | actiontest.txt
5 | againexample.txt
6 | atlas shrank.txt
7 | blank.txt
8 | blockfaker.txt
9 | bouncers.txt
10 | byyourside.txt
11 | cakemonsters.txt
12 | castlecloset.txt
13 | castlemouse.txt
14 | chaos wizard.txt
15 | coincounter.txt
16 | collapse.txt
17 | color chained.txt
18 | constellationz.txt
19 | cratopia.txt
20 | cute train.txt
21 | dropswap.txt
22 | dungeon janitor.txt
23 | easyenigma.txt
24 | ebony and ivory.txt
25 | gobble_rush.txt
26 | heroes_of_sokoban.txt
27 | heroes_of_sokoban_2.txt
28 | heroes_of_sokoban_3.txt
29 | icecrates.txt
30 | it dies in the light.txt
31 | kettle.txt
32 | kishoutenketsu.html
33 | ledchallenge.txt
34 | leftrightnpcs.txt
35 | legend of zokoban.txt
36 | limerick.txt
37 | lovendpieces.txt
38 | lunar_lockout.txt
39 | m c eschers armageddon.txt
40 | manic_ammo.txt
41 | mazetest.txt
42 | mazezam.txt
43 | microban.txt
44 | midas.txt
45 | modality.txt
46 | naughtysprite.txt
47 | nekopuzzle.txt
48 | notsnake.txt
49 | octat.txt
50 | ponies jumping synchronously.txt
51 | push.txt
52 | puzzles.txt
53 | randomrobots.txt
54 | randomspawner.txt
55 | rigid_and_little.txt
56 | rigid_one_object.txt
57 | rigid_scott1.txt
58 | rigidfail1.txt
59 | riverpuzzle.txt
60 | robotarm.txt
61 | scriptcross.txt
62 | several_rigid_bodies.txt
63 | slidings.txt
64 | smother.txt
65 | sok7.txt
66 | sokoban_basic.txt
67 | sokoban_eyeball.txt
68 | sokoban_horizontal.txt
69 | sokoban_match3.txt
70 | sokoban_sticky.txt
71 | sokobond demake.txt
72 | stick_candy_puzzle_saga.txt
73 | sumo.txt
74 | take heart lass.txt
75 | the_saga_of_the_candy_scroll.txt
76 | threes.txt
77 | tiny treasure hunt.txt
78 | tunnel rat.txt
79 | twolittlecrates1.txt
80 | twolittlecrates2.txt
81 | twolittlecrates3.txt
82 | twolittlecrates4.txt
83 | whaleworld.txt
84 | wordgame.txt
85 | wrappingrecipe.txt
86 | zenpuzzlegarden.txt
87 |
--------------------------------------------------------------------------------
/src/tests/resources/testingFrameWork.js:
--------------------------------------------------------------------------------
1 | function runTest(dataarray) {
2 | unitTesting=true;
3 | levelString=dataarray[0];
4 | errorStrings = []
5 | warningStrings = []
6 |
7 | for (const s of errorStrings)
8 | {
9 | throw s
10 | }
11 |
12 | const inputDat = dataarray[1]
13 | const targetlevel = dataarray[3] || 0
14 | const randomseed = dataarray[4] || null
15 |
16 | compile(targetlevel, levelString, randomseed)
17 |
18 | if (errorStrings.length > 0)
19 | return false
20 |
21 | while (againing)
22 | {
23 | againing = false
24 | processInput(processing_causes.again_frame)
25 | }
26 |
27 | for(const val of inputDat)
28 | {
29 | if (val === "undo") {
30 | execution_context.doUndo()
31 | } else if (val === "restart") {
32 | DoRestart()
33 | } else if (val === "tick") {
34 | processInput(processing_causes.autotick)
35 | } else {
36 | processInput(val)
37 | }
38 | while (againing)
39 | {
40 | againing = false
41 | processInput(processing_causes.again_frame)
42 | }
43 | }
44 |
45 | unitTesting = false
46 | return (level.convertToString( (dataarray[2].indexOf('=') >= 0) ? '=' : ':' ) === dataarray[2])
47 | }
48 |
49 | function runCompilationTest(game_string, recordedErrorStrings, recordedWarningStrings)
50 | {
51 | unitTesting = true
52 | errorStrings = []
53 | warningStrings = []
54 |
55 | try{
56 | compile(null, game_string)
57 | } catch (error){
58 | console.log(error)
59 | }
60 |
61 | return error_message_equal(errorStrings, recordedErrorStrings) && error_message_equal(warningStrings, recordedWarningStrings)
62 | }
63 |
--------------------------------------------------------------------------------
/src/tests/resources/tests.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | var inputVals = {0 : 'U', 1: 'L', 2:'D', 3:'R', 4:' Action ', tick:' Tick ', undo:' Undo ', restart:' Restart '}
4 |
5 | // tests of results of inputs
6 | function test_module_with_inputs(testdata_list)
7 | {
8 | for (const [testname, td] of testdata_list)
9 | {
10 | const [testcode, testinput, testresult] = td
11 | const level_num = td[3]||0
12 | const seed = td[4] // undefined is ok
13 | const input = testinput.map( j => inputVals[j] ).join('').replaceAll(/([^t\s]{5})(?=[^\s])/gu, '$1 ').replaceAll(/\s\s+/g, ' ')
14 | const description = "level: " + level_num + "input: " + input + ' Game: ' + testcode + ' '
15 | test(
16 | testname,
17 | [ [testname, testcode, input, testresult, level_num, seed ], ['test name', 'game code', 'input', 'expected level state', 'level number', 'random seed'] ],
18 | function(tdat)
19 | {
20 | const display_content = description
21 | return function()
22 | {
23 | ok(
24 | runTest(tdat),
25 | ((errorStrings.length > 0) ? ('Got errors: ' + errorStrings.map(m => '' + JSON.stringify(stripHTMLTags(m)) + ' ').join('') + ' ') : '') +
26 | ((warningStrings.length > 0) ? ('Got warnings: ' + warningStrings.map(m => '' + JSON.stringify(stripHTMLTags(m)) + ' ').join('') + ' ') : '')
27 | + display_content
28 | )
29 | };
30 | }(td)
31 | )
32 | }
33 | }
34 |
35 | QUnit.module('Game parts') // replay game parts to check the execution of rules
36 | test_module_with_inputs(testdata)
37 |
38 | QUnit.module('Increpare Games') // replay game parts to check the execution of rules
39 | test_module_with_inputs(increpare_testdata)
40 |
41 |
42 |
43 |
44 | QUnit.module('Errors and warnings') // check that they are well triggered
45 |
46 | function test_compile(testcode, errors, warnings)
47 | {
48 | return function()
49 | {
50 | const testerrors = 'Expected errors: ' + errors.map( m => ''+JSON.stringify(m)+' ').join('') + ' '
51 | const testwarnings = 'Expected warnings: ' + warnings.map(m => ''+JSON.stringify(m)+' ').join('') + ' '
52 | ok(runCompilationTest(testcode, errors, warnings),
53 | testerrors + testwarnings +
54 | 'Got errors: ' + errorStrings.map(m => '' + JSON.stringify(stripHTMLTags(m)) + ' ').join('') + ' ' +
55 | 'Got warnings: ' + warningStrings.map(m => '' + JSON.stringify(stripHTMLTags(m)) + ' ').join('') + ' ' +
56 | 'Game: ' + testcode + ' '
57 | )
58 | }
59 | }
60 |
61 | for (const [testname, td] of errormessage_testdata)
62 | {
63 | test(
64 | testname,
65 | [ [testname, td[0]], ['test name', 'game code'] ],
66 | test_compile(td[0], td[1], td[2])
67 | )
68 | }
69 |
70 |
71 | QUnit.module('Demos') // Test that demos compile without error or warning
72 |
73 | function get_textfile(filename, callback)
74 | {
75 | var fileOpenClient = new XMLHttpRequest()
76 | fileOpenClient.open('GET', filename)
77 | fileOpenClient.onreadystatechange = function()
78 | {
79 | if(fileOpenClient.readyState == 4)
80 | {
81 | callback(fileOpenClient.responseText)
82 | }
83 | }
84 | fileOpenClient.send()
85 | }
86 |
87 | get_textfile('demo_list.txt', demo_list => demo_list.split('\n').forEach(test_demo_file) )
88 |
89 | function test_demo_file(demo_filename)
90 | {
91 | if (demo_filename === 'README' || demo_filename === 'blank.txt' || demo_filename === '')
92 | return
93 | get_textfile('../demo/'+demo_filename, function(demo_text)
94 | {
95 | // const errormessage_entry = errormessage_testdata.findIndex( ([name, data]) => data[0].replace(/\s/g, '') === demo_text.replace(/\s/g, ''))
96 | // if (errormessage_entry >= 0)
97 | // console.log('can erase entry #'+errormessage_entry+' ('+errormessage_testdata[errormessage_entry][0]+') of error messages, as it is the same as '+demo_filename)
98 | test(
99 | demo_filename,
100 | [ [demo_text, demo_filename], ['game code', 'filename'] ],
101 | test_compile(demo_text, [], [])
102 | )
103 | })
104 | }
105 |
--------------------------------------------------------------------------------
/src/tests/resources/wrapper.js:
--------------------------------------------------------------------------------
1 | var lastDownTarget = null;
2 | var canvas = null;
3 | var input = document.createElement('TEXTAREA');
4 |
5 | function canvasResize() {}
6 |
7 | function redraw() {}
8 |
9 | function forceRegenImages() {}
10 |
11 | var levelString;
12 | var inputString;
13 | var outputString;
14 |
15 | function consolePrintFromRule(text){}
16 | function consolePrint(text) {}
17 | function consoleError(text) {
18 | // window.console.log(text);
19 | }
20 |
21 | function consoleCacheDump() {}
22 | var editor = {
23 | getValue : function () { return levelString }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/src/tests/tests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | QUnit Tests for Pattern:Script
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------