File Coverage

blib/lib/Game/TextMapper.pm
Criterion Covered Total %
statement 104 166 62.6
branch 4 40 10.0
condition 14 93 15.0
subroutine 28 31 90.3
pod 0 7 0.0
total 150 337 44.5


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             # License: AGPL 3 (see below)
3             package Game::TextMapper;
4              
5             our $VERSION = 1.09;
6              
7 11     11   717059 use Game::TextMapper::Log;
  11         37  
  11         456  
8 11     11   6292 use Game::TextMapper::Point;
  11         31  
  11         66  
9 11     11   5540 use Game::TextMapper::Line;
  11         33  
  11         69  
10 11     11   5638 use Game::TextMapper::Mapper::Hex;
  11         43  
  11         157  
11 11     11   6090 use Game::TextMapper::Mapper::Square;
  11         36  
  11         120  
12 11     11   5685 use Game::TextMapper::Smale;
  11         29  
  11         83  
13 11     11   5242 use Game::TextMapper::Apocalypse;
  11         33  
  11         68  
14 11     11   7360 use Game::TextMapper::Gridmapper;
  11         42  
  11         97  
15 11     11   22402 use Game::TextMapper::Schroeder::Alpine;
  11         59  
  11         81  
16 11     11   6184 use Game::TextMapper::Schroeder::Archipelago;
  11         29  
  11         80  
17 11     11   6280 use Game::TextMapper::Schroeder::Island;
  11         36  
  11         68  
18 11     11   6195 use Game::TextMapper::Traveller;
  11         35  
  11         160  
19 11     11   5849 use Game::TextMapper::Folkesten;
  11         37  
  11         77  
20 11     11   5951 use Game::TextMapper::Solo;
  11         33  
  11         71  
21              
22 11     11   525 use Modern::Perl '2018';
  11         21  
  11         52  
23 11     11   8489 use Mojolicious::Lite;
  11         673734  
  11         59  
24 11     11   311168 use Mojo::DOM;
  11         26  
  11         370  
25 11     11   74 use Mojo::Util qw(url_escape xml_escape);
  11         25  
  11         749  
26 11     11   69 use File::ShareDir 'dist_dir';
  11         21  
  11         502  
27 11     11   6563 use Pod::Simple::HTML;
  11         152515  
  11         589  
28 11     11   5492 use Pod::Simple::Text;
  11         53872  
  11         522  
29 11     11   78 use List::Util qw(none);
  11         22  
  11         840  
30 11     11   60 use Cwd;
  11         19  
  11         72957  
31              
32             # Commands for the command line!
33             push @{app->commands->namespaces}, 'Game::TextMapper::Command';
34              
35             # Change scheme if "X-Forwarded-Proto" header is set (presumably to HTTPS)
36             app->hook(before_dispatch => sub {
37             my $c = shift;
38             $c->req->url->base->scheme('https')
39             if $c->req->headers->header('X-Forwarded-Proto') } );
40              
41             plugin Config => {
42             default => {
43             loglevel => 'warn',
44             logfile => undef,
45             contrib => undef,
46             },
47             file => getcwd() . '/text-mapper.conf',
48             };
49              
50             my $log = Game::TextMapper::Log->get;
51             $log->level(app->config('loglevel'));
52             $log->path(app->config('logfile'));
53             $log->info($log->path ? "Logfile is " . $log->path : "Logging to stderr");
54              
55             my $dist_dir = app->config('contrib') // dist_dir('Game-TextMapper');
56             $log->debug("Reading contrib files from $dist_dir");
57              
58             get '/' => sub {
59             my $c = shift;
60             my $param = $c->param('map');
61             if ($param) {
62             my $mapper;
63             if ($c->param('type') and $c->param('type') eq 'square') {
64             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
65             } else {
66             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
67             }
68             $mapper->initialize($param);
69             $c->render(text => $mapper->svg, format => 'svg');
70             } else {
71             my $mapper = new Game::TextMapper::Mapper;
72             my $map = $mapper->initialize('')->example();
73             $c->render(template => 'edit', map => $map);
74             }
75             };
76              
77             any '/edit' => sub {
78             my $c = shift;
79             my $mapper = new Game::TextMapper::Mapper;
80             my $map = $c->param('map') || $mapper->initialize('')->example();
81             $c->render(map => $map);
82             };
83              
84             any '/render' => sub {
85             my $c = shift;
86             my $mapper;
87             if ($c->param('type') and $c->param('type') eq 'square') {
88             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
89             } else {
90             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
91             }
92             $mapper->initialize($c->param('map'));
93             $c->render(text => $mapper->svg, format => 'svg');
94             };
95              
96             get '/:type/redirect' => sub {
97             my $self = shift;
98             my $type = $self->param('type');
99             my $rooms = $self->param('rooms');
100             my $seed = $self->param('seed');
101             my $caves = $self->param('caves');
102             my %params = ();
103             $params{rooms} = $rooms if $rooms;
104             $params{seed} = $seed if $seed;
105             $params{caves} = $caves if $caves;
106             $self->redirect_to($self->url_for($type . "random")->query(%params));
107             } => 'redirect';
108              
109             # alias for /smale
110             get '/random' => sub {
111             my $c = shift;
112             my $bw = $c->param('bw');
113             my $width = $c->param('width');
114             my $height = $c->param('height');
115             $c->render(template => 'edit', map => Game::TextMapper::Smale->new->generate_map($width, $height, $bw));
116             };
117              
118             get '/smale' => sub {
119             my $c = shift;
120             my $bw = $c->param('bw');
121             my $width = $c->param('width');
122             my $height = $c->param('height');
123             if ($c->stash('format')||'' eq 'txt') {
124             $c->render(text => Game::TextMapper::Smale->new->generate_map($width, $height));
125             } else {
126             $c->render(template => 'edit',
127             map => Game::TextMapper::Smale->new->generate_map($width, $height, $bw));
128             }
129             };
130              
131             get '/smale/random' => sub {
132             my $c = shift;
133             my $bw = $c->param('bw');
134             my $width = $c->param('width');
135             my $height = $c->param('height');
136             my $map = Game::TextMapper::Smale->new->generate_map($width, $height, $bw);
137             my $svg = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir)
138             ->initialize($map)
139             ->svg();
140             $c->render(text => $svg, format => 'svg');
141             };
142              
143             get '/smale/random/text' => sub {
144             my $c = shift;
145             my $bw = $c->param('bw');
146             my $width = $c->param('width');
147             my $height = $c->param('height');
148             my $text = Game::TextMapper::Smale->new->generate_map($width, $height, $bw);
149             $c->render(text => $text, format => 'txt');
150             };
151              
152             sub alpine_map {
153 2     2 0 5 my $c = shift;
154             # must be able to override this for the documentation
155 2   33     15 my $step = shift // $c->param('step');
156             # need to compute the seed here so that we can send along the URL
157 2   33     538 my $seed = $c->param('seed') || int(rand(1000000000));
158 2         150 my $url = $c->url_with('alpinedocument')->query({seed => $seed})->to_abs;
159 2         1613 my @params = ($c->param('width'),
160             $c->param('height'),
161             $c->param('steepness'),
162             $c->param('peaks'),
163             $c->param('peak'),
164             $c->param('bumps'),
165             $c->param('bump'),
166             $c->param('bottom'),
167             $c->param('arid'),
168             $c->param('climate'),
169             $c->param('wind'),
170             $seed,
171             $url,
172             $step,
173             );
174 2   100     1137 my $type = $c->param('type') // 'hex';
175 2 100       109 if ($type eq 'hex') {
176 1         29 return Game::TextMapper::Schroeder::Alpine
177             ->with_roles('Game::TextMapper::Schroeder::Hex')->new()
178             ->generate_map(@params);
179             } else {
180 1         13 return Game::TextMapper::Schroeder::Alpine
181             ->with_roles('Game::TextMapper::Schroeder::Square')->new()
182             ->generate_map(@params);
183             }
184             }
185              
186             get '/alpine' => sub {
187             my $c = shift;
188             my $map = alpine_map($c);
189             if ($c->stash('format') || '' eq 'txt') {
190             $c->render(text => $map);
191             } else {
192             $c->render(template => 'edit', map => $map);
193             }
194             };
195              
196             get '/alpine/random' => sub {
197             my $c = shift;
198             my $map = alpine_map($c);
199             my $type = $c->param('type') // 'hex';
200             my $mapper;
201             if ($type eq 'hex') {
202             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
203             } else {
204             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
205             }
206             my $svg = $mapper->initialize($map)->svg;
207             $c->render(text => $svg, format => 'svg');
208             };
209              
210             get '/alpine/random/text' => sub {
211             my $c = shift;
212             my $map = alpine_map($c);
213             $c->render(text => $map, format => 'txt');
214             };
215              
216             get '/alpine/document' => sub {
217             my $c = shift;
218             # prepare a map for every step
219             my @maps;
220             my $type = $c->param('type') || 'hex';
221             # use the same seed for all the calls
222             my $seed = $c->param('seed');
223             if (not defined $seed) {
224             $seed = int(rand(1000000000));
225             $c->param('seed' => $seed);
226             }
227             # We'd like to use a smaller map because it is so slow, so default to height 5.
228             $c->param('height' => 5) unless $c->param('height');
229             # Let's remember the $data so we can query it for the parameters used.
230             my ($map, $data);
231             for my $step (1 .. 19) {
232             ($map, $data) = alpine_map($c, $step);
233             my $mapper;
234             if ($type eq 'hex') {
235             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
236             } else {
237             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
238             }
239             my $svg = $mapper->initialize($map)->svg;
240             $svg =~ s/<\?xml version="1.0" encoding="UTF-8" standalone="no"\?>\n//g;
241             push(@maps, $svg);
242             };
243             $c->stash("maps" => \@maps);
244              
245             # Generate the documentation text based on the stashed maps.
246             $c->render(template => 'alpine_document',
247             seed => $seed,
248             width => $data->width,
249             height => $data->height,
250             steepness => $data->steepness,
251             peaks => $data->peaks,
252             peak => $data->peak,
253             bumps => $data->bumps,
254             bump => $data->bump,
255             bottom => $data->bottom,
256             arid => $data->arid,
257             climate => $data->climate);
258             };
259              
260             get '/alpine/random/interactive' => sub {
261             my $c = shift;
262             my $map = alpine_map($c);
263             my $type = $c->param('type') // 'hex';
264             my $mapper;
265             if ($type eq 'hex') {
266             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
267             } else {
268             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
269             }
270             my $svg = $mapper->initialize($map)->svg;
271             $c->render(template => 'alpine_interactive',
272             map => $svg);
273             };
274              
275             get '/alpine/parameters' => sub {
276             my $c = shift;
277             $c->render(template => 'alpine_parameters');
278             };
279              
280             get '/folkesten' => sub {
281             my $c = shift;
282             if ($c->stash('format')||'' eq 'txt') {
283             $c->render(text => Game::TextMapper::Folkesten->new->generate_map());
284             } else {
285             $c->render(template => 'edit',
286             map => Game::TextMapper::Folkesten->new->generate_map());
287             }
288             };
289              
290             get '/folkesten/random' => sub {
291             my $c = shift;
292             my $map = Game::TextMapper::Folkesten->new->generate_map();
293             my $svg = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir)
294             ->initialize($map)
295             ->svg();
296             $c->render(text => $svg, format => 'svg');
297             };
298              
299             get '/folkesten/random/text' => sub {
300             my $c = shift;
301             my $text = Game::TextMapper::Folkesten->new->generate_map();
302             $c->render(text => $text, format => 'txt');
303             };
304              
305             get '/solo' => sub {
306             my $c = shift;
307             my $mapper = Game::TextMapper::Solo->new($c->req->params->to_hash);
308             if ($c->stash('format')||'' eq 'txt') {
309             $c->render(text => $mapper->generate_map());
310             } else {
311             $c->render(template => 'edit', map => $mapper->generate_map());
312             }
313             };
314              
315             get '/solo/random' => sub {
316             my $c = shift;
317             my $mapper = Game::TextMapper::Solo->new($c->req->params->to_hash);
318             my $map = $mapper->generate_map();
319             my $svg = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir)
320             ->initialize($map)
321             ->svg();
322             $c->render(text => $svg, format => 'svg');
323             };
324              
325             get '/solo/random/text' => sub {
326             my $c = shift;
327             my $mapper = Game::TextMapper::Solo->new($c->req->params->to_hash);
328             my $text = $mapper->generate_map();
329             $c->render(text => $text, format => 'txt');
330             };
331              
332             # does not handle z coordinates
333             sub border_modification {
334 0     0 0 0 my ($map, $top, $left, $right, $bottom, $empty) = @_;
335 0         0 my (@lines, @temp, %seen);
336 0         0 my ($x, $y, $points, $text);
337 0         0 my ($minx, $miny, $maxx, $maxy);
338             # shift map around
339 0         0 foreach (split(/\r?\n/, $map)) {
340 0 0       0 if (($x, $y, $text) = /^(\d\d)(\d\d)\s+(.*)/) {
    0          
341 0 0 0     0 $minx = $x if not defined $minx or $x < $minx;
342 0 0 0     0 $miny = $y if not defined $miny or $y < $miny;
343 0 0 0     0 $maxx = $x if not defined $maxx or $x > $maxx;
344 0 0 0     0 $maxy = $y if not defined $maxy or $y > $maxy;
345 0         0 my $point = Game::TextMapper::Point->new(x => $x + $left, y => $y + $top);
346 0 0       0 $seen{$point->coordinates} = 1 if $empty;
347 0         0 push(@lines, [$point, $text]);
348             } elsif (($points, $text) = /^(-?\d\d-?\d\d(?:--?\d\d-?\d\d)+)\s+(.*)/) {
349 0         0 my @numbers = $points =~ /\G(-?\d\d)(-?\d\d)-?/cg;
350 0         0 my @points;
351 0         0 while (@numbers) {
352 0         0 my ($x, $y) = splice(@numbers, 0, 2);
353 0         0 push(@points, Game::TextMapper::Point->new(x => $x + $left, y => $y + $top));
354             }
355 0         0 push(@lines, [Game::TextMapper::Line->new(points => \@points), $text]);
356             } else {
357 0         0 push(@lines, $_);
358             }
359             }
360             # only now do we know the extent of the map
361 0         0 $maxx += $left + $right;
362 0         0 $maxy += $top + $bottom;
363             # with that information we can now determine what lies outside the map
364 0         0 @temp = ();
365 0         0 foreach (@lines) {
366 0 0       0 if (ref) {
367 0         0 my ($it, $text) = @$_;
368 0 0       0 if (ref($it) eq 'Game::TextMapper::Point') {
369 0 0 0     0 if ($it->x <= $maxx and $it->x >= $minx
      0        
      0        
370             and $it->y <= $maxy and $it->y >= $miny) {
371 0         0 push(@temp, $_);
372             }
373             } else { # Game::TextMapper::Line
374             my $outside = none {
375 0 0 0 0   0 ($_->x <= $maxx and $_->x >= $minx
      0        
376             and $_->y <= $maxy and $_->y >= $miny)
377 0         0 } @{$it->points};
  0         0  
378 0 0       0 push(@temp, $_) unless $outside;
379             }
380             } else {
381 0         0 push(@temp, $_);
382             }
383             }
384 0         0 @lines = @temp;
385             # add missing hexes, if requested
386 0 0       0 if ($empty) {
387 0         0 for $x ($minx .. $maxx) {
388 0         0 for $y ($miny .. $maxy) {
389 0         0 my $point = Game::TextMapper::Point->new(x => $x, y => $y);
390 0 0       0 if (not $seen{$point->coordinates}) {
391 0         0 push(@lines, [$point, "empty"]);
392             }
393             }
394             }
395             # also, sort regions before trails before others
396             @lines = sort {
397 0         0 (# arrays before strings
398 0 0 0     0 ref($b) cmp ref($a)
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
399             # string comparison if both are strings
400             or not(ref($a)) and not(ref($b)) and $a cmp $b
401             # if we get here, we know both are arrays
402             # points before lines
403             or ref($b->[0]) cmp ref($a->[0])
404             # if both are points, compare the coordinates
405             or ref($a->[0]) eq 'Game::TextMapper::Point' and $a->[0]->cmp($b->[0])
406             # if both are lines, compare the first two coordinates (the minimum line length)
407             or ref($a->[0]) eq 'Game::TextMapper::Line' and ($a->[0]->points->[0]->cmp($b->[0]->points->[0])
408             or $a->[0]->points->[1]->cmp($b->[0]->points->[1]))
409             # if bot are the same point (!) …
410             or 0)
411             } @lines;
412             }
413             $map = join("\n",
414             map {
415 0 0       0 if (ref) {
  0         0  
416 0         0 my ($it, $text) = @$_;
417 0 0       0 if (ref($it) eq 'Game::TextMapper::Point') {
418 0         0 Game::TextMapper::Point::coord($it->x, $it->y) . " " . $text
419             } else {
420 0         0 my $points = $it->points;
421             join("-",
422 0         0 map { Game::TextMapper::Point::coord($_->x, $_->y) } @$points)
  0         0  
423             . " " . $text;
424             }
425             } else {
426 0         0 $_;
427             }
428             } @lines) . "\n";
429 0         0 return $map;
430             }
431              
432             any '/borders' => sub {
433             my $c = shift;
434             my $map = border_modification(map { $c->param($_) } qw(map top left right bottom empty));
435             $c->param('map', $map);
436             $c->render(template => 'edit', map => $map);
437             };
438              
439             sub island_map {
440 2     2 0 17 my $c = shift;
441             # must be able to override this for the documentation
442 2   33     19 my $step = shift // $c->param('step');
443             # need to compute the seed here so that we can send along the URL
444 2   33     440 my $seed = $c->param('seed') || int(rand(1000000000));
445 2         137 my $url = $c->url_with('islanddocument')->query({seed => $seed})->to_abs;
446 2         1573 my @params = ($c->param('width'),
447             $c->param('height'),
448             $c->param('radius'),
449             $seed,
450             $url,
451             $step,
452             );
453 2   100     235 my $type = $c->param('type') // 'hex';
454 2 100       76 if ($type eq 'hex') {
455 1         51 return Game::TextMapper::Schroeder::Island
456             ->with_roles('Game::TextMapper::Schroeder::Hex')->new()
457             ->generate_map(@params);
458             } else {
459 1         14 return Game::TextMapper::Schroeder::Island
460             ->with_roles('Game::TextMapper::Schroeder::Square')->new()
461             ->generate_map(@params);
462             }
463             }
464              
465             get '/island' => sub {
466             my $c = shift;
467             my $map = island_map($c);
468             if ($c->stash('format') || '' eq 'txt') {
469             $c->render(text => $map);
470             } else {
471             $c->render(template => 'edit', map => $map);
472             }
473             };
474              
475             get '/island/random' => sub {
476             my $c = shift;
477             my $map = island_map($c);
478             my $type = $c->param('type') // 'hex';
479             my $mapper;
480             if ($type eq 'hex') {
481             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
482             } else {
483             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
484             }
485             my $svg = $mapper->initialize($map)->svg;
486             $c->render(text => $svg, format => 'svg');
487             };
488              
489             sub archipelago_map {
490 0     0 0 0 my $c = shift;
491             # must be able to override this for the documentation
492 0   0     0 my $step = shift // $c->param('step');
493             # need to compute the seed here so that we can send along the URL
494 0   0     0 my $seed = $c->param('seed') || int(rand(1000000000));
495 0         0 my $url = $c->url_with('archipelagodocument')->query({seed => $seed})->to_abs;
496 0         0 my @params = ($c->param('width'),
497             $c->param('height'),
498             $c->param('concentration'),
499             $c->param('eruptions'),
500             $c->param('top'),
501             $c->param('bottom'),
502             $seed,
503             $url,
504             $step,
505             );
506 0   0     0 my $type = $c->param('type') // 'hex';
507 0 0       0 if ($type eq 'hex') {
508 0         0 return Game::TextMapper::Schroeder::Archipelago
509             ->with_roles('Game::TextMapper::Schroeder::Hex')->new()
510             ->generate_map(@params);
511             } else {
512 0         0 return Game::TextMapper::Schroeder::Archipelago
513             ->with_roles('Game::TextMapper::Schroeder::Square')->new()
514             ->generate_map(@params);
515             }
516             }
517              
518             get '/archipelago' => sub {
519             my $c = shift;
520             my $map = archipelago_map($c);
521             if ($c->stash('format') || '' eq 'txt') {
522             $c->render(text => $map);
523             } else {
524             $c->render(template => 'edit', map => $map);
525             }
526             };
527              
528             get '/archipelago/random' => sub {
529             my $c = shift;
530             my $map = archipelago_map($c);
531             my $type = $c->param('type') // 'hex';
532             my $mapper;
533             if ($type eq 'hex') {
534             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
535             } else {
536             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
537             }
538             my $svg = $mapper->initialize($map)->svg;
539             $c->render(text => $svg, format => 'svg');
540             };
541              
542             sub gridmapper_map {
543 1     1 0 3 my $c = shift;
544 1   33     7 my $seed = $c->param('seed') || int(rand(1000000000));
545 1   50     319 my $pillars = $c->param('pillars') // 1;
546 1   50     94 my $rooms = $c->param('rooms') // 5;
547 1   50     104 my $caves = $c->param('caves') // 0;
548 1         81 srand($seed);
549 1         27 return Game::TextMapper::Gridmapper->new()
550             ->generate_map($pillars, $rooms, $caves);
551             }
552              
553             get '/gridmapper' => sub {
554             my $c = shift;
555             my $map = gridmapper_map($c);
556             if ($c->stash('format') || '' eq 'txt') {
557             $c->render(text => $map);
558             } else {
559             $c->render(template => 'edit', map => $map);
560             }
561             };
562              
563             get '/gridmapper/random' => sub {
564             my $c = shift;
565             my $map = gridmapper_map($c);
566             my $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
567             my $svg = $mapper->initialize($map)->svg;
568             $c->render(text => $svg, format => 'svg');
569             };
570              
571             get '/gridmapper/random/text' => sub {
572             my $c = shift;
573             my $map = gridmapper_map($c);
574             $c->render(text => $map, format => 'txt');
575             };
576              
577             sub apocalypse_map {
578 1     1 0 3 my $c = shift;
579 1   33     6 my $seed = $c->param('seed') || int(rand(1000000000));
580 1         295 srand($seed);
581 1         6 my $hash = $c->req->params->to_hash;
582 1         72 return Game::TextMapper::Apocalypse->new(%$hash)
583             ->generate_map();
584             }
585              
586             get '/apocalypse' => sub {
587             my $c = shift;
588             my $map = apocalypse_map($c);
589             if ($c->stash('format') || '' eq 'txt') {
590             $c->render(text => $map);
591             } else {
592             $c->render(template => 'edit', map => $map);
593             }
594             };
595              
596             get '/apocalypse/random' => sub {
597             my $c = shift;
598             my $map = apocalypse_map($c);
599             my $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
600             my $svg = $mapper->initialize($map)->svg;
601             $c->render(text => $svg, format => 'svg');
602             };
603              
604             get '/apocalypse/random/text' => sub {
605             my $c = shift;
606             my $map = apocalypse_map($c);
607             $c->render(text => $map, format => 'txt');
608             };
609              
610             sub star_map {
611 1     1 0 3 my $c = shift;
612 1   33     4 my $seed = $c->param('seed') || int(rand(1000000000));
613 1         189 srand($seed);
614 1         3 my $hash = $c->req->params->to_hash;
615 1         36 return Game::TextMapper::Traveller->new(%$hash)->generate_map();
616             }
617              
618             get '/traveller' => sub {
619             my $c = shift;
620             my $map = star_map($c);
621             if ($c->stash('format') || '' eq 'txt') {
622             $c->render(text => $map);
623             } else {
624             $c->render(template => 'edit', map => $map);
625             }
626             };
627              
628             get '/traveller/random' => sub {
629             my $c = shift;
630             my $map = star_map($c);
631             my $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
632             my $svg = $mapper->initialize($map)->svg;
633             $c->render(text => $svg, format => 'svg');
634             };
635              
636             get '/traveller/random/text' => sub {
637             my $c = shift;
638             my $map = star_map($c);
639             $c->render(text => $map, format => 'txt');
640             };
641              
642             get '/help' => sub {
643             my $c = shift;
644              
645             seek(DATA,0,0);
646             local $/ = undef;
647             my $pod = ;
648             $pod =~ s/=head1 NAME\n.*=head1 DESCRIPTION/=head1 Text Mapper/gs;
649             my $parser = Pod::Simple::HTML->new;
650             $parser->html_header_after_title('');
651             $parser->html_header_before_title('');
652             $parser->title_prefix('');
654             my $html;
655             $parser->output_string(\$html);
656             $parser->parse_string_document($pod);
657              
658             my $dom = Mojo::DOM->new($html);
659             for my $pre ($dom->find('pre')->each) {
660             my $map = $pre->text;
661             $map =~ s/^ //mg;
662             next if $map =~ /^perl/; # how to call it
663             my $url = $c->url_for('render')->query(map => $map);
664             $pre->replace("
" . xml_escape($map) . "
\n"
665             . qq{

Render this example

});
666             }
667              
668             $c->render(html => $dom);
669             };
670              
671             app->start;
672              
673             __DATA__