forked from Germanunkol/trAInsported
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDocumentation.html
More file actions
636 lines (469 loc) · 24.5 KB
/
Documentation.html
File metadata and controls
636 lines (469 loc) · 24.5 KB
1
2
3
4
5
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
<h1><strong>trAInsported</strong></h1>
<hr />
<p>This is a list of all functions that your AI should use. </p>
<p>Before you read this, check out the tutorials in-game! </p>
<p>For more information, visit the website: <br />
<a href="http://trainsportedgame.no-ip.org">Official trAInsported Website</a></p>
<hr />
<p>This document is split up into two parts. <br />
The first part lists the events which you should define. If you define them correctly, then the game will call them if certain events happen. For example, if you define "function ai.init()" then this function will be called when the round starts. Or, if your script has a function "function ai.chooseDirection()" then this function will be called whenever a train comes to a junction and wants to know which way to go. <br />
The second part of this document lists functions that you can call in your code, like "random", "getMoney" and "print".</p>
<p>Many of these are covered in the tutorials, so make sure to play them!</p>
<h2>Callback Events</h2>
<h3>function ai.init(map, money, maximumTrains)</h3>
<p>This event is called, at the beginning of the round. The current map is passed to it, so you can analize it, get it ready for pathfinding and search it for important junctions, Hotspots, shortest paths etc. <br />
<strong>Passed Arguments:</strong></p>
<ul>
<li>map: A 2D representation of the current map. "map" has a field map.width and a field map.height to check the size of the map. It also holds information about what's on the map, which is stored by x and y coordinates. For example, to see what's at the position x=3, y=5, you only have to check map[3][5]. A map field can be filled with: "C" (Connected Rail), "H" (House), "S" (Hotspot)</li>
<li>money: The amount of money that you currently have. You can use this to check how many trains you may buy (one train costs 25 credits).</li>
<li>maximumTrains: the max number of trains each AI is allowed to buy on this map.</li>
</ul>
<p><strong>Example</strong></p>
<pre><code> function ai.init(map, money)
-- go through the entire map and search for all hotspots:
for x = 1, map.width, 1 do
for y = 1, map.height, 1 do
if map[x][y] == "S" then -- if the field at [x][y] is "S" then print the coordinates on the screen:
print("Hotspot found at: " .. x .. ", " .. y .. "!")
end
end
end
buyTrain(random(map.width), random(map.height)) -- place train at random position
end
</code></pre>
<h3>function ai.newTrain(train)</h3>
<p>Called when the train you bought using buyTrain has successfully been created. <br />
<strong>Passed Arguments:</strong></p>
<ul>
<li>train: a Lua table representing the train. See ai.chooseDirection for details.</li>
</ul>
<p><strong>Example</strong></p>
<pre><code> function ai.newTrain(train)
print("Bought new train:", train.name)
print("Train is starting at:", train.x, train.y)
end
</code></pre>
<h3>function ai.chooseDirection(train, possibleDirections)</h3>
<p>Called just before a train enters a junction. This function is essential: It lets your train tell the game where it wants to go. If this function returns a valid direction (N, E, S or W) and the direction is not blocked by another train, the train will continue in that direction. </p>
<p><strong>Passed Arguments</strong></p>
<ul>
<li>train: a Lua table representing the train. It has the fields:
<ul>
<li>train.ID (an ID representing the train - number)</li>
<li>train.name (the name of the train - string)</li>
<li>train.x (x-position of the tile the train is on - number)</li>
<li>train.y (y-position of the tile the train is on - number)</li>
<li>train.dir (the direction the train is going in - "N", "S", "E" or "W")</li>
<li>train.passenger (table representing passenger who's currently on the train, if any - holds the passengers name, destX and destY. See ai.foundPassengers for more details.)</li>
<li>(<strong>NOTE:</strong> The trains' .x and .y fields are the coordinates of the tile it's coming from. So in this function, there's also a .nextX and .nextY field to describe the tile of the junction the train is moving to. Not all functions that get a train have these fields, usually just .x and .y do the job.)</li>
</ul></li>
<li>possibleDirections: a Lua table showing the directions which the train can choose from. One of these directions should be returned by the function, to make the train go that way. The table's indieces are the direction ("N","S","E","W") and if the value at the index is true, then this direction is one that the train can move in.</li>
</ul>
<p><strong>Returns</strong></p>
<ul>
<li>The function should return a string holding one of the directions which are stored in possibleDirections ("N", "S", "E", "W" are the possible values). If a wrong value is returned, or nothing is returned then the game will automatically try the directions in the following order: N, S, E, W</li>
</ul>
<p><strong>Example</strong></p>
<pre><code> function ai.chooseDirection(train, possibleDirections)
-- let train 1 go South if possible
if train.ID == 1 then
if possibleDirection["S"] == true then
return "S"
end
end
-- let all trains go North if possible
if possibleDirections["N"] == true then
return "N"
end
-- if the above directions were not possible, the let the game choose a direction (i.e. choose nothing)
end
</code></pre>
<h3>function ai.blocked(train, possibleDirections, prevDirection)</h3>
<p>Called after a train was blocked by another train and can't move in that direction. By returning the prevDirection, you can keep trying to go in the same direction. However, you should not try to keep moving in the same direction forever, because then trains could block each other for the rest of the match. <br />
<strong>Passed Arguments</strong></p>
<ul>
<li>train: same as above (see ai.chooseDirection)</li>
<li>possibleDirections: a Lua table showing the directions which the train can choose from. One of these directions should be returned by the function, to make the train go that way. The table's indieces are the direction ("N","S","E","W") and if the value at the index is true, then this direction is one that the train can move in.</li>
<li>prevDirection: the direction that the train previously tried to move in, but was blocked.</li>
</ul>
<p><strong>Returns</strong></p>
<ul>
<li>The function should return a string holding one of the directions which are stored in possibleDirections ("N", "S", "E", "W" are the possible values). If a wrong value is returned, or nothing is returned then the game will automatically try the directions in the following order: N, S, E, W</li>
</ul>
<p><strong>Example</strong></p>
<pre><code> function ai.blocked(train, possibleDirections, prevDirection)
if prevDirection == "N" then -- if i've tried North before, then try South, then East, then West
if possibleDirections["S"] == true then
return "S"
elseif possibleDirections["E"] == true then
return "E"
elseif possibleDirections["W"] == true then
return "W"
else return "N"
end
elseif prevDirection == "S" then
if possibleDirections["E"] == true then
return "E"
elseif possibleDirections["W"] == true then
return "W"
elseif possibleDirections["N"] == true then
return "N"
else return "S"
end
elseif prevDirection == "E" then
if possibleDirections["W"] == true then
return "W"
elseif possibleDirections["N"] == true then
return "N"
elseif possibleDirections["S"] == true then
return "S"
else return "E"
end
else
if possibleDirections["N"] == true then
return "N"
elseif possibleDirections["S"] == true then
return "S"
elseif possibleDirections["E"] == true then
return "E"
else return "W"
end
end
end
</code></pre>
<h3>function ai.foundPassengers(train, passengers)</h3>
<p>This function is called when a train arrives at a position where passengers are waiting to be picked up. If one of the passengers in the list is returned, then this passenger is picked up (but only if the train does not have a passenger at the moment). If you want to pick up a passenger but you're already trainsporting another passenger, you can drop the current passenger using the function 'dropPassenger'. <br />
<strong>Passed Arguments:</strong></p>
<ul>
<li>train: the train which has arrived at a position where passengers are waiting. Same as above (see ai.chooseDirection)</li>
<li>passengers: List of passengers which are at this position. Each passenger is a table containing:
<ul>
<li>name: Name of the passenger</li>
<li>destX: Destination of the passenger (X-coordinate)</li>
<li>destY: Destination of the passenger (Y-coordinate)</li>
</ul></li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>Passenger: The passenger in the list who is to be picked up. (i.e. passengers[1] or passengers[2])</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> function ai.foundPassengers(train, passengers)
print("I'll pick up passenger: " .. passengers[1].name)
print("Taking him to: " .. passenger[1].destX .. ", " .. passenger[1].destY)
return passengers[1]
end
</code></pre>
<h3>function ai.foundDestination(train)</h3>
<p>This function is called when a train which is carrying a passenger arrives at the position where the passenger wants to go. This way, you can drop off the passenger by calling 'dropPassenger'. <br />
<strong>Passed Arguments</strong></p>
<ul>
<li>train: same as above (see ai.chooseDirection)</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>nothing</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> function ai.foundDestination(train)
print("Dropping of my passenger @ " .. train.x .. ", " .. train.y)
dropPassenger(train)
end
</code></pre>
<h3>function ai.enoughMoney(money)</h3>
<p>Whenever the player earns money, the game checks if the player now has enough money to buy a new train. If that is the case, then this function is called so that the player can decide whether to buy a new train by calling buyTrain() and if so, where to place it. <br />
<strong>Passed Arguments</strong></p>
<ul>
<li>money: The amount of money you now have.</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>nothing</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> rememberMap = nil
function ai.enoughMoney(money)
x = random(rememberMap.width) -- important! this only works because the map was stored in the global "rememberap" in ai.init()
y = random(rememberMap.height)
while money >= 25 do -- 25c is cost of one train
buyTrain(x, y)
money = money - 25
end
end
function ai.init(map, money)
rememberMap = map
buyTrain(random(map.width), random(map.height))
end
</code></pre>
<h3>function ai.newPassenger(name, x, y, destX, destY, vipTime)</h3>
<p>Will be called whenever a new passenger spawns on the map, or if a passenger has been dropped off at a place that was not his destination. <br />
<strong>Passed Arguments:</strong></p>
<ul>
<li>name: Name of the passenger. If the passenger is a VIP, then "[VIP]" is appended to his name.</li>
<li>x, y: The x and y positions of the tile on which the passenger is standing.</li>
<li>destX, destY: The x and y position of the tile to which the passenger would like to be transported. As soon as you drop him/her of at this position, you will get paid.</li>
<li>vipTime: If the passenger is a VIP, then this is the time in seconds until he stops being a vip. Each VIP has a vipTime that's dependant on the distance they want to travel. <strong>Careful</strong>, if the passenger is not a VIP then this value will be nil! So always check: "if vipTime then ..." before you do anything with it. If you want to work with the vipTime, os.time() migth be a good function to check out - this way you can get the time passed between two times.</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>nothing</li>
</ul>
<p><strong>Example</strong></p>
<pre><code> passengerList = {} -- create an empty list to save the passengers in
function ai.newPassenger(name, x, y, destX, destY)
-- create a new table which holds the info about the new passenger:
local passenger = {x=x, y=y, destX=destX, destY=destY}
-- save the passenger into the global list, to "remember" him for later use.
-- use the name as an index to easily find the passenger later on.
passengerList[name] = passenger
end
</code></pre>
<h3>function ai.passengerBoarded(train, passenger)</h3>
<p>Will be called whenever a train of another player has taken a passenger aboard. You can use this function to make sure your trains no longer try to go to that passenger, if that was their plan. <br />
Note: This function is NOT called when one of your own trains takes a passenger aboard! <br />
<strong>Passed Arguments:</strong></p>
<ul>
<li>train: the train which has taken the passenger in. (Same as for chooseDirection)</li>
<li>passenger: name of passenger who has boarded a train.</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>nothing</li>
</ul>
<p><strong>Example</strong></p>
<pre><code> passengerList = {} -- create an empty list to save the passengers in
function ai.newPassenger(name, x, y, destX, destY)
-- create a new table which holds the info about the new passenger:
local passenger = {x=x, y=y, destX=destX, destY=destY}
-- save the passenger into the global list, to "remember" him for later use.
-- use the name as an index to easily find the passenger later on.
passengerList[name] = passenger
end
function ai.passengerBoarded(train, passenger)
-- set the entry in the passengerList for the passenger to nil. This is the accepted way of "deleting" the entry in Lua.
passengerList[passenger] = nil
end
</code></pre>
<h3>function ai.mapEvent(...)</h3>
<p>Some challenge-matches need more events than listed above, to notify you if something has happened. <br />
These challenge maps will call this function. This way, a user-created map can tell you, for example, when the time's almost up, or when your train has arrived at a certain, special rail-piece. <br />
The description of the map should always make sure you know what arguments the map creator will pass to you. <br />
<strong>Passed Arguments:</strong></p>
<ul>
<li>any number of arguments which the creator of the map has specified. Check the map description to see what arguments to expect!</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>nothing</li>
</ul>
<h2>Available Functions</h2>
<p>You can define your own functions inside your code. <br />
This is a list of functions that are already specified and which you can call at any time. Be careful not to overuse them - they all take some time to compute and your code will be aborted if it takes too long!</p>
<h3>getNumberOfLines()</h3>
<p>Every one of your events (ai.init, ai.chooseDirection etc) gets aborted after a certain number of lua line executions. A line often - but not always - corresponds to a line of code. This function lets you see how many executions you have left before the game will abort your function. <br />
An important note: Calling game functions (like "print") will also use up lines. "print()", for example, uses up quite a lot of lines. <br />
Also, ai.init gets to use a lot more lines than the other functions. Try to do heavy calculations in ai.init()! </p>
<p><strong>Returns:</strong></p>
<ul>
<li>number of lines already used</li>
<li>total number of lines you can use in this function</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> -- use this to see how many lines of code you can execute in ai.chooseDirection():
function ai.chooseDirection(train, directions)
print(getNumberOfLines())
-- the rest of your code.
end
</code></pre>
<h3>mapTime()</h3>
<p>Get the time in seconds that have passed since the map started. Note - this time is multiplied by the speed multiplyer, so if the game is sped up, this still gives the correct time.
If you want to know the system time without multiplication then use os.time instead.</p>
<p><strong>Returns:</strong></p>
<ul>
<li>seconds since round start</li>
<li>time when the game ends (Only if the game mode is the "Time" mode! Otherwise it does not return a second value!)</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> function ai.init()
time, totalTime = mapTime()
if totalTime then
print("Game type is 'time' - Game will end after " .. .. " seconds!")
end
end
function ai.chooseDirection()
time, totalTime = mapTime()
print( totalTime - time .. " seconds left until round ends.")
end
</code></pre>
<h3>print(...)</h3>
<p>Prints the given objects to the ingame-console (make sure it's visible by pressing 'C' in the game!) <br />
<strong>Arguments:</strong></p>
<ul>
<li>... Comma-seperated list of objects to print</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>nothing</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> print("Sheldon likes trains!", 1, tbl[1])
</code></pre>
<h3>clearConsole()</h3>
<p>Clears the ingame console to make space for new Messages. <br />
<strong>Arguments:</strong></p>
<ul>
<li>none</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>nothing</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> clearConsole()
</code></pre>
<h3>pairs(tbl)</h3>
<p>The standard Lua pairs value. See a Lua documentation for more examples. <br />
<strong>Arguments:</strong></p>
<ul>
<li>tbl: The Table through which you want to iterate</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>Iterator over the key, value pairs in the table.</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> for k, value in pairs(map) do
print(k, value)
end
</code></pre>
<h3>type(variable)</h3>
<p>Returns the type of the given variable. <br />
<strong>Arguments:</strong></p>
<ul>
<li>variable: variable of which you want to know the type</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>Type of variable. Possible Types are: "string", "number", "table", "userdata"</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> if type(myValue) == "number" then
print("my Value is a number!")
elseif type(myValue) == "string" then
print("my Value is a string!")
else
print("my Value is neither a number, nor a string. It's of type " .. type(myValue))
end
</code></pre>
<h3>pcall(chunk, args)</h3>
<p>Will safely execute the code given by chunk (can be a function). This way, you can run code which might raise an error or might not be working correctly, without loosing control. In case there's an error in the code, you can safely handle the exception. <br />
<strong>Arguments:</strong></p>
<ul>
<li>chunk: code to run, usually the name of the function</li>
<li>args: any arguments that should be passed to the chunk</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>ok: true if the chunk ran without errors, false otherwise</li>
<li>result: data returned by the chunk if it ran correctly. Otherwise, result holds the error message.</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> function foo( argument )
... -- do stuff in here
end
bar = 1
ok, result = pcall(foo, bar) -- call the function "foo" with the argument "bar"
if not ok then
print("Error in 'foo': " .. result)
end
</code></pre>
<h3>error(msg)</h3>
<p>Throws an error. If the function in which the error is thrown is called using pcall, then the pcall will return this error as its error message. Otherwise, the error is printed in the ingame console.</p>
<p><strong>Arguments:</strong></p>
<ul>
<li>msg: any error message or error code.</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> -- absolutely useless code (but works)
function foo( b )
a = 1
while a < 100 do
if b > a then
error("b is greater than a!")
end
a = a + 1
end
end
ok, msg = pcall(foo, 10)
if not ok then
print("Error: " .. msg)
end
</code></pre>
<h3>table, math, string functions</h3>
<p>You also have access to all of Lua's table-functions: table.sort, table.insert, table.remove etc. See a Lua Documentation for details.
Same goes for math functions (math.sin, math.cos, math.floor, math.random etc) and the string functions (string.sub, string.find etc).
There is a few exceptions, but most functions in these libraries are available.</p>
<h3>dropPassenger(train)</h3>
<p>Will drop of the passenger of the train at its current position.</p>
<h3>buyTrain(x, y, dir)</h3>
<p>Will try to buy a train and place it at the position [X][Y]. If there's no rail at the given position, the game will place the train at the rail closest to [X][Y]. If the rail is blocked, the game waits until the blocking trains have left the rail. This means the train WILL be bought, but might be placed at a different position or go in a different direction than what you anticipate.</p>
<p><strong>Arguments:</strong></p>
<ul>
<li>x (The x-coordinate of the position at which to place the train)</li>
<li>y (The y-coordinate of the position)</li>
<li>dir (The direction in which the train should go. If an invalid direction is passed (or if it's 'nil') then the game tries to use default directions in the following order: North, South, East, West.</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> function ai.init(map, money)
while money > 25 do -- check if I still have enough money to buy a train?
money = money - 25 -- 25 is the standard cost for a train
xPos = random(map.width)
yPos = random(map.height)
randomDir = random(4)
if randomDir == 1 then
buyTrain(xPos, yPos, "N") -- try to buy a train, place it at the position and make it go North.
elseif randomDir == 2
buyTrain(xPos, yPos, "S") -- try to buy a train, place it at the position and make it go South.
elseif randomDir == 3
buyTrain(xPos, yPos, "E") -- try to buy a train, place it at the position and make it go South.
elseif randomDir == 4
buyTrain(xPos, yPos, "W") -- try to buy a train, place it at the position and make it go South.
else
buyTrain(xPos, yPos) -- don't care what direction to go in.
end
end
end
</code></pre>
<h3>getMoney()</h3>
<p><strong>Arguments:</strong></p>
<ul>
<li>none</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>The amount of credits the player currently has at his or her disposal.</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code> myMoney = getMoney()
if myMoney > 10 then
print("I'm rich!")
else
print("I'm not so rich...")
end
</code></pre>
<h3>require(filename), dofile(filename), loadfile(filename)</h3>
<p>These functions are used to add additional files to your code. There is a few things to consider:
- Paths given here are <em>relative</em> to the main file of your AI.
- Best practice is to put all included files into a subfolder of the AI folder. This way, the subfiles will not be handled as individual AIs by the game.
- When uploading, you need to make a .zip file out of all the files the AI uses. Make sure the main AI is <em>at the root</em> of this .zip file. To check if this is the case: Open your created .zip file in any archive manager and check if the main .lua file is right there. If it is, you're fine, if not (i.e. you have to go into a subfolder inside the archive to find it) then you need to change that.
- Your uploaded .zip file must not be larger than the file limit given by the website.
- Require is just an interface to dofile for security reasons. It can be used to include files, but don't expect it to be as complex as a proper require call is.
Check the online lua documentation for more details on the various functions.</p>
<p><strong>Arguments:</strong></p>
<ul>
<li>The file to load or run. See Lua documentation for details.</li>
</ul>
<p><strong>Returns:</strong></p>
<ul>
<li>A chunk of code that you can run, and error codes. See Lua documentation for more details.</li>
</ul>