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

Render this example

});
680             }
681              
682             $c->render(html => $dom);
683             };
684              
685             app->start;
686              
687             __DATA__