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

Render this example

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