File Coverage

blib/lib/Games/Dukedom.pm
Criterion Covered Total %
statement 99 574 17.2
branch 3 178 1.6
condition 4 75 5.3
subroutine 32 59 54.2
pod 7 8 87.5
total 145 894 16.2


line stmt bran cond sub pod time code
1             package Games::Dukedom;
2              
3             our $VERSION = 'v0.1_1';
4              
5             our $DEBUG = 0;
6              
7 4     4   96927 use Storable qw( freeze thaw );
  4         13693  
  4         348  
8 4     4   27 use Carp;
  4         4  
  4         271  
9              
10 4     4   1804 use Games::Dukedom::Signal;
  4         14  
  4         180  
11              
12 4     4   32 use Moo;
  4         7  
  4         16  
13 4     4   3567 use MooX::StrictConstructor;
  4         10592  
  4         24  
14 4     4   16671 use MooX::ClassAttribute;
  4         16686  
  4         22  
15              
16 4         63 use MooX::Struct -rw, Land => [
17             qw(
18             +delta
19             +trades
20             +spoils
21             +price
22             +sell_price
23             +planted
24             )
25             ],
26             Population => [
27             qw(
28             +delta
29             +starvations
30             +levy
31             +casualties
32             +looted
33             +diseased
34             +deaths
35             +births
36             )
37             ],
38             Grain => [
39             qw(
40             +delta
41             +food
42             +trades
43             +seed
44             +spoilage
45             +wages
46             +spoils
47             +yield
48             +expense
49             +taxes
50             )
51             ],
52             War => [
53             qw(
54             +first_strike
55             +potential
56             +desire
57             +will
58             +grain_damage
59             )
60 4     4   2957 ];
  4         555847  
61              
62             # status codes
63 4     4   12846 use constant RUNNING => 0;
  4         9  
  4         302  
64 4     4   34 use constant RETIRED => 1;
  4         6  
  4         221  
65 4     4   20 use constant KINGDOM => 2;
  4         8  
  4         202  
66 4     4   21 use constant DEPOSED => -1;
  4         5  
  4         187  
67 4     4   24 use constant ABOLISHED => -2;
  4         5  
  4         248  
68 4     4   26 use constant QUIT_GAME => -3;
  4         11  
  4         189  
69              
70             # magic numbers
71 4     4   20 use constant RUNNING => 0;
  4         7  
  4         288  
72 4     4   22 use constant TAX_RATE => .5;
  4         5  
  4         182  
73 4     4   24 use constant MAX_YEAR => 45;
  4         11  
  4         312  
74 4     4   25 use constant MIN_LAND => 45;
  4         5  
  4         186  
75 4     4   21 use constant MIN_POPULATION => 33;
  4         4  
  4         229  
76 4     4   24 use constant MIN_GRAIN => 429;
  4         10  
  4         182  
77 4     4   21 use constant MAX_FOOD_BONUS => 4;
  4         6  
  4         210  
78 4     4   21 use constant MAX_SALE => 4000;
  4         5  
  4         190  
79 4     4   20 use constant MAX_SELL_TRIES => 3;
  4         6  
  4         177  
80 4     4   19 use constant MIN_LAND_PRICE => 4;
  4         7  
  4         185  
81 4     4   19 use constant MIN_EXPENSE => 429;
  4         4  
  4         187  
82 4     4   20 use constant WAR_CONSTANT => 1.95;
  4         6  
  4         210  
83 4     4   18 use constant UNREST_FACTOR => .85;
  4         63  
  4         188  
84 4     4   20 use constant MAX_1YEAR_UNREST => 88;
  4         3  
  4         185  
85 4     4   18 use constant MAX_TOTAL_UNREST => 99;
  4         6  
  4         30782  
86              
87             my @steps = (
88             qw(
89             _init_year
90             _feed_the_peasants
91             _starvation_and_unrest
92             _purchase_land
93             _war_with_the_king
94             _grain_production
95             _kings_levy
96             _war_with_neigbor
97             _population_changes
98             _harvest_grain
99             _update_unrest
100             )
101             );
102              
103             my %traits = (
104             price => {
105             q1 => 4,
106             q2 => 7,
107             },
108             yield => {
109             q1 => 4,
110             q2 => 8,
111             },
112             spoilage => {
113             q1 => 4,
114             q2 => 6,
115             },
116             levies => {
117             q1 => 3,
118             q2 => 8,
119             },
120             war => {
121             q1 => 5,
122             q2 => 8,
123             },
124             first_strike => {
125             q1 => 3,
126             q2 => 6,
127             },
128             disease => {
129             q1 => 3,
130             q2 => 8,
131             },
132             birth => {
133             q1 => 4,
134             q2 => 8,
135             },
136             merc_quality => {
137             q1 => 8,
138             q2 => 8,
139             },
140             );
141              
142             my $fnr = sub {
143             my ( $q1, $q2 ) = @_;
144              
145             return int( rand() * ( 1 + $q2 - $q1 ) ) + $q1;
146             };
147              
148             my $gauss = sub {
149             my ( $q1, $q2 ) = @_;
150              
151             my $g0;
152              
153             my $q3 = &$fnr( $q1, $q2 );
154             if ( &$fnr( $q1, $q2 ) > 5 ) {
155             $g0 = ( $q3 + &$fnr( $q1, $q2 ) ) / 2;
156             }
157             else {
158             $g0 = $q3;
159             }
160              
161             return $g0;
162             };
163              
164             my $print_msg = sub {
165             my $msg = shift;
166              
167             print $msg;
168              
169             return;
170             };
171              
172             my $input_yn = sub {
173             my $default = shift || '';
174              
175             my $ans = <>;
176             chomp($ans);
177             $ans ||= $default;
178              
179             return ( $ans =~ /^(?:q|quit)\s*$/i || $ans =~ /^(?:y|n)$/i )
180             ? lc($ans)
181             : undef;
182             };
183              
184             my $input_value = sub {
185             my $default = shift || 0;
186              
187             my $ans = <>;
188             chomp($ans);
189             $ans = $default unless length($ans);
190              
191             return ( $ans =~ /^(?:q|quit)\s*$/i || $ans !~ /\D/ ) ? $ans : undef;
192             };
193              
194             class_has signal => (
195             is => 'ro',
196             init_arg => undef,
197             default => 'Games::Dukedom::Signal',
198             handles => 'Throwable',
199             );
200              
201             class_has show_msg => (
202             is => 'rw',
203             default => sub { $print_msg },
204             );
205              
206             class_has get_yn => (
207             is => 'rw',
208             default => sub { $input_yn },
209             );
210              
211             class_has get_value => (
212             is => 'rw',
213             default => sub { $input_value },
214             );
215              
216             has _base_values => (
217             is => 'ro',
218             init_arg => undef,
219             default => sub {
220             my $base = {};
221             for ( keys(%traits) ) {
222             $base->{$_} = &$gauss( $traits{$_}{q1}, $traits{$_}{q2} );
223             }
224             return $base;
225             },
226             );
227              
228             has year => (
229             is => 'rwp',
230             init_arg => undef,
231             default => 0,
232             );
233              
234             has population => (
235             is => 'rwp',
236             init_arg => undef,
237             default => 100,
238             );
239              
240             has _population => (
241             is => 'ro',
242             lazy => 1,
243             clearer => 1,
244             default => sub { Population->new; },
245             init_arg => undef,
246             );
247              
248             has grain => (
249             is => 'rwp',
250             init_arg => undef,
251             default => 4177,
252             );
253              
254             has _grain => (
255             is => 'ro',
256             clearer => 1,
257             lazy => 1,
258             default => sub { Grain->new; },
259             init_arg => undef,
260             );
261              
262             has land => (
263             is => 'rwp',
264             init_arg => undef,
265             default => 600,
266             );
267              
268             has _land => (
269             is => 'ro',
270             lazy => 1,
271             clearer => 1,
272             default => sub { Land->new; },
273             init_arg => undef,
274             );
275              
276             has land_fertility => (
277             is => 'ro',
278             init_arg => undef,
279             default => sub {
280             {
281             100 => 216,
282             80 => 200,
283             60 => 184,
284             40 => 0,
285             20 => 0,
286             0 => 0,
287             };
288             },
289             );
290              
291             has war => (
292             is => 'rwp',
293             init_arg => undef,
294             default => 0,
295             );
296              
297             has _war => (
298             is => 'ro',
299             lazy => 1,
300             clearer => 1,
301             default => sub { War->new; },
302             init_arg => undef,
303             );
304              
305             has yield => (
306             is => 'rwp',
307             init_arg => undef,
308             default => 3.95,
309             );
310              
311             has unrest => (
312             is => 'rwp',
313             init_arg => undef,
314             default => 0,
315             );
316              
317             has _unrest => (
318             is => 'ro',
319             default => 0,
320             init_arg => undef,
321             );
322              
323             has king_unrest => (
324             is => 'rwp',
325             init_arg => undef,
326             default => 0,
327             );
328              
329             has black_D => (
330             is => 'ro',
331             init_arg => undef,
332             default => 0,
333             );
334              
335             has input => (
336             is => 'rw',
337             init_arg => undef,
338             clearer => 1,
339             default => undef,
340             );
341              
342             has _steps => (
343             is => 'lazy',
344             init_arg => undef,
345             clearer => 1,
346             default => sub { [@steps] },
347             );
348              
349             has status => (
350             is => 'rwp',
351             init_arg => undef,
352             default => RUNNING,
353             );
354              
355             has _msg => (
356             is => 'rw',
357             init_arg => undef,
358             clearer => 1,
359             default => undef,
360             );
361              
362             sub BUILD {
363 3     3 0 38 my $self = shift;
364              
365 3         66 return;
366             }
367              
368             # guarantee we have a clean input if needed. this avoids a potential
369             # problem if the "clear_" behavior changes.
370             before throw => sub {
371             my $self = shift;
372             my %params = @_;
373              
374             $self->clear_input;
375              
376             return;
377             };
378              
379             around input => sub {
380             my $orig = shift;
381             my $self = shift;
382             my $input = $_[0] || '';
383              
384             $self->_next_step('_quit_game') if $input =~ /^(?:q|quit)\s*$/i;
385              
386             return $self->$orig(@_);
387             };
388              
389             sub input_is_yn {
390 17     17 1 68 my $self = shift;
391              
392 17         506 my $value = $self->input;
393 17 50       62 chomp($value) if defined($value);
394              
395 17   66     174 return !!( defined($value) && $value =~ /^(?:y|n)$/i );
396             }
397              
398             sub input_is_value {
399 19     19 1 75 my $self = shift;
400              
401 19         568 my $value = $self->input;
402 19 50       69 chomp($value) if defined($value);
403              
404 19   66     221 return !!( defined($value) && ( $value =~ /^\d+$/ ) );
405             }
406              
407             sub play_one_year {
408 0     0 1 0 my $self = shift;
409 0         0 my $params = @_;
410              
411 0         0 while ( @{ $self->_steps } ) {
  0         0  
412 0         0 my $step = shift( @{ $self->_steps } );
  0         0  
413              
414 0         0 $self->$step;
415 0         0 $self->clear_input;
416             }
417              
418             #print $self->end_of_year_report();
419              
420 0         0 $self->_clear_steps;
421 0         0 $self->_clear_population;
422 0         0 $self->_clear_grain;
423 0         0 $self->_clear_land;
424 0         0 $self->_clear_war;
425              
426 0         0 $self->_end_of_game_check;
427              
428 0         0 return;
429             }
430              
431             sub game_over {
432 0     0 1 0 my $self = shift;
433              
434 0         0 return !!( $self->status != RUNNING );
435             }
436              
437             sub _next_step {
438 4     4   6 my $self = shift;
439 4         8 my $next = shift;
440              
441 4 50       24 croak 'Illegal value for "_next_step"' unless $self->can($next);
442              
443 4         8 return unshift( @{ $self->_steps }, $next );
  4         92  
444             }
445              
446             sub _randomize {
447 0     0     my $self = shift;
448 0           my $trait = shift;
449              
450 0           return int( &$fnr( -2, 2 ) + $self->_base_values->{$trait} );
451             }
452              
453             sub _init_year {
454 0     0     my $self = shift;
455              
456 0           ++$self->{year};
457              
458 0           $self->_population->delta( $self->population );
459 0           $self->_grain->delta( $self->grain );
460 0           $self->_land->delta( $self->land );
461              
462 0           $self->{_unrest} = 0;
463              
464 0           $self->_land->{price} =
465             int( ( 2 * $self->yield ) + $self->_randomize('price') - 5 );
466 0 0         $self->_land->{price} = MIN_LAND_PRICE
467             if $self->_land->price < MIN_LAND_PRICE;
468              
469 0           $self->_land->{sell_price} = $self->_land->price;
470              
471 0           $self->{_msg} = $self->summary_report;
472 0           $self->{_msg} .= $self->fertility_report;
473              
474 0           $self->_next_step('_display_msg');
475              
476 0           return;
477             }
478              
479             sub _display_msg {
480 0     0     my $self = shift;
481              
482 0           $self->throw( display => $self->_clear_msg );
483             }
484              
485             sub summary_report {
486 0     0 1   my $self = shift;
487              
488 0           my $msg = sprintf( "\nYear %d Peasants %d Land %d Grain %d\n",
489             $self->year, $self->population, $self->land, $self->grain );
490              
491 0           return $msg;
492             }
493              
494             sub fertility_report {
495 0     0 1   my $self = shift;
496              
497 0           my $msg = "Land Fertility:\n";
498 0           $msg .= " 100% 80% 60% 40% 20% Depl\n";
499 0           for ( 100, 80, 60, 40, 20, 0 ) {
500 0           $msg .= sprintf( "%5d", $self->land_fertility->{$_} );
501             }
502 0           $msg .= "\n";
503              
504 0           return $msg;
505             }
506              
507             sub _feed_the_peasants {
508 0     0     my $self = shift;
509              
510 0 0         my $hint = ( $self->grain / $self->population ) < 11 ? $self->grain : 14;
511              
512 0 0 0       $self->_next_step('_feed_the_peasants')
513             and $self->throw(
514             display => "Grain for food [$hint]: ",
515             action => 'get_value',
516             default => $hint,
517             ) unless $self->input_is_value;
518              
519 0           my $food = $self->input;
520              
521             # shortcut
522 0 0 0       $food *= $self->population if ( $food < 100 && $self->grain > $food );
523              
524 0 0 0       if ( $food > $self->grain ) {
    0          
525 0           $self->_next_step('_feed_the_peasants');
526              
527 0           $self->throw( display => $self->_insufficient_grain('feed') );
528             }
529             elsif (( ( $food / $self->population ) < 11 )
530             && ( $food != $self->grain ) )
531             {
532 0           $self->{_unrest} += 3;
533              
534 0           $self->_next_step('_feed_the_peasants');
535              
536 0           my $msg = "The peasants demonstrate before the castle\n";
537 0           $msg .= "with sharpened scythes\n\n";
538 0           $self->throw( display => $msg );
539             }
540              
541 0           $self->_grain->{food} = -$food;
542 0           $self->{grain} += $self->_grain->{food};
543              
544 0           return;
545             }
546              
547             sub _starvation_and_unrest {
548 0     0     my $self = shift;
549              
550 0           my $food = -$self->_grain->food;
551              
552 0           my $x1 = $food / $self->population;
553 0 0         if ( $x1 < 13 ) {
554 0           $self->_population->{starvations} =
555             -int( ( $self->population - ( $food / 13 ) ) );
556 0           $self->{population} += $self->_population->starvations;
557             }
558              
559             # only allow bonus for extra food up to 18HL/peasant
560 0           $x1 -= 14;
561 0 0         $x1 = MAX_FOOD_BONUS if $x1 > MAX_FOOD_BONUS;
562              
563 0           $self->{_unrest} =
564             $self->_unrest - ( 3 * $self->_population->starvations ) - ( 2 * $x1 );
565              
566 0 0         if ( $self->_population->starvations < 0 ) {
567 0           $self->_msg("Some peasants have starved during the winter\n");
568 0           $self->_next_step('_display_msg');
569             }
570              
571 0   0       return ( ( $self->_unrest > 88 ) || ( $self->population < 33 ) );
572             }
573              
574             sub _purchase_land {
575 0     0     my $self = shift;
576              
577 0           my $land = $self->_land;
578 0           my $grain = $self->_grain;
579              
580 0           my $msg = sprintf( "Land to buy at %d HL/HA [0]: ", int( $land->{price} ) );
581 0 0 0       $self->_next_step('_purchase_land')
582             and $self->throw( display => $msg, action => 'get_value', default => 0 )
583             unless $self->input_is_value();
584              
585 0 0 0       $self->_next_step('_sell_land') and return
586             unless my $buy = $self->input;
587              
588 0 0 0       $self->_next_step('_purchase_land')
589             and $self->throw( display => $self->_insufficient_grain('buy') )
590             if ( $buy * $land->price > $self->grain );
591              
592 0           $self->land_fertility->{60} += $buy;
593 0           $land->{trades} = $buy;
594 0           $self->{land} += $buy;
595 0           $grain->{trades} = -$buy * $land->{price};
596 0           $self->{grain} += $grain->{trades};
597              
598 0           return;
599             }
600              
601             sub _sell_land {
602 0     0     my $self = shift;
603              
604 0           my $land = $self->_land;
605 0           my $grain = $self->_grain;
606              
607 0 0         if ( $land->price - $land->sell_price > MAX_SELL_TRIES ) {
608 0           $grain->{trades} = 0;
609              
610 0           $self->throw( display => "Buyers have lost interest\n" );
611             }
612              
613 0           my $x1 = 0;
614 0           for ( 100, 80, 60 ) {
615 0           $x1 += $self->land_fertility->{$_};
616             }
617              
618 0           my $price = --$land->{sell_price};
619              
620 0           my $msg = sprintf( "Land to sell at %d HL/HA [0]: ", $price );
621 0 0 0       $self->_next_step('_sell_land')
622             and $self->throw( display => $msg, action => 'get_value', default => 0 )
623             unless $self->input_is_value();
624              
625 0 0         return unless my $sold = $self->input;
626              
627 0           $self->{_msg} = undef;
628 0 0         if ( $sold > $x1 ) {
    0          
629 0           $self->_next_step('_display_msg');
630 0           $self->{_msg} = sprintf( "You only have %d HA. of good land\n", $x1 );
631             }
632             elsif ( ( $grain->{trades} = $sold * $price ) > MAX_SALE ) {
633 0           $self->_next_step('_display_msg');
634 0           $self->{_msg} = "No buyers have that much grain - sell less\n";
635             }
636 0 0         return if $self->_msg;
637              
638 0           $land->{trades} = -$sold;
639              
640 0           my $valid = 0;
641 0           my $sold_q;
642 0           for ( 60, 80, 100 ) {
643 0           $sold_q = $_;
644 0 0         if ( $sold <= $self->land_fertility->{$_} ) {
645 0           $valid = 1;
646 0           last;
647             }
648             else {
649 0           $sold -= $self->land_fertility->{$_};
650 0           $self->land_fertility->{$_} = 0;
651             }
652             }
653              
654 0 0         if ( !$valid ) {
655 0           my $msg = "LAND SELLING LOOP ERROR - CONTACT PROGRAM AUTHOR IF\n";
656 0           $msg .= "ERROR IS NOT YOURS IN ENTERING PROGRAM,\n";
657 0           $msg .= "AND SEEMS TO BE FAULT OF PROGRAM'S LOGIC.\n";
658              
659 0           die $msg;
660             }
661              
662 0           $self->land_fertility->{$sold_q} -= $sold;
663 0           $self->land += $land->trades;
664              
665 0 0         $self->_set_status(ABOLISHED) if $self->land < 10;
666              
667 0           $msg = '';
668 0 0 0       if ( ( $price < MIN_LAND_PRICE ) && $sold ) {
669 0           $grain->{trades} = int( $grain->{trades} / 2 );
670 0           $msg = "\nThe High King appropriates half your earnings\n";
671 0           $msg .= "as punishment for selling at such a low price\n";
672             }
673              
674 0           $self->{grain} += $grain->{trades};
675 0 0         $self->throw( display => $msg ) if $msg;
676              
677 0           return;
678             }
679              
680             sub _war_with_the_king {
681 0     0     my $self = shift;
682              
683 0 0         $self->_king_wants_war if $self->king_unrest > 0;
684              
685 0 0         return if $self->king_unrest > -2;
686              
687 0           my $mercs = int( $self->grain / 100 );
688              
689 0           my $msg = "\nThe King's army is about to attack your duchy\n";
690 0           $msg .= sprintf( "You have hired %d foreign mercenaries\n", $mercs );
691 0           $msg .= "at 100 HL. each (payment in advance)\n\n";
692              
693             # assuming a population > 200 at the time of your revolt, # the C source
694             # i ported from allowed one to win with as few as 5 mercs.
695             # if ( ( $self->grain * $mercs ) + $self->population > 2399 ) {
696             #
697             # the Java source changed it so it took significantly more, about 275 but
698             # was still a fixed value.
699             # if ( ( 8 * $mercs ) + $self->population > 2399 ) {
700             #
701             # i've added an element of chance. again, assuming a populaton of 200, it
702             # now requires anywhere from 219 to 366 mercs to win depending on the
703             # quality of merc you hire. this means you must have at least 22,000 in
704             # grain to win, and as much as 37,000 if your mercs suck.
705 0 0         if ( ( int( $self->_randomize('merc_quality') ) * $mercs ) +
706             $self->population > 2399 )
707             {
708 0           $msg .= "Wipe the blood from the crown - you are now High King!\n\n";
709 0           $msg .= "A nearby monarchy threatens war; ";
710 0           $msg .= "how many .........\n\n\n\n";
711              
712 0           $self->_set_status(KINGDOM);
713             }
714             else {
715 0           $msg .= "The placement of your head atop the castle gate signifies\n";
716 0           $msg .= "that the High King has abolished your Ducal right\n\n";
717              
718 0           $self->_set_status(ABOLISHED);
719             }
720              
721 0           $self->{_msg} = $msg;
722 0           $self->{_steps} = ['_display_msg'];
723              
724 0           return;
725             }
726              
727             sub _king_wants_war {
728 0     0     my $self = shift;
729              
730 0 0         return unless $self->king_unrest > 0;
731              
732 0           my $msg = "The King demands twice the royal tax in the\n";
733 0           $msg .= 'hope of provoking war. Will you pay? [Y/n]: ';
734              
735 0 0 0       $self->_next_step('_king_wants_war')
736             and $self->throw( display => $msg, action => 'get_yn', default => 'Y' )
737             unless $self->input_is_yn;
738              
739 0           my $ans = $self->input;
740 0   0       $ans ||= 'Y';
741              
742 0 0         $self->_set_king_unrest( ( $ans =~ /^n/i ) ? -1 : 2 );
743              
744 0           return;
745             }
746              
747             sub _grain_production {
748 0     0     my $self = shift;
749              
750 0           my $done = 0;
751              
752 0           my $pop_plant = $self->population * 4;
753 0           my $grain_plant = int( $self->grain / 2 );
754 0 0         my $max_grain_plant =
755             $grain_plant > $self->land ? $self->land : $grain_plant;
756 0 0         my $max_plant =
757             $pop_plant > $max_grain_plant ? $max_grain_plant : $pop_plant;
758              
759 0           my $msg = sprintf( "Land to plant [%d]: ", $max_plant );
760 0 0 0       $self->_next_step('_grain_production')
761             and $self->throw(
762             display => $msg,
763             action => 'get_value',
764             default => $max_plant
765             ) unless $self->input_is_value();
766              
767 0   0       my $plant = $self->input || $max_plant;
768              
769 0           my $grain = $self->_grain;
770 0           $msg = '';
771              
772 0 0         if ( $plant > $self->land ) {
773 0           $msg = "You don't have enough land\n";
774 0           $msg .= sprintf( "You only have %d HA. of land left\n", $self->land );
775             }
776 0 0         if ( $plant > ( 4 * $self->population ) ) {
777 0           $msg = "You don't have enough peasants\n";
778 0           $msg .= sprintf( "Your peasants can only plant %d HA. of land\n",
779             4 * $self->population );
780             }
781 0           $grain->{seed} = -2 * $plant;
782 0 0         if ( -$grain->seed > $self->grain ) {
783 0           $msg = $self->_insufficient_grain('plant');
784             }
785              
786 0 0         if ($msg) {
787 0           $self->_next_step('_grain_production');
788 0           $self->throw( display => $msg );
789             }
790              
791 0           $grain->{yield} = $plant;
792 0           $self->_land->{planted} = $plant;
793 0           $self->{grain} += $grain->seed;
794              
795 0           my $tmp_quality = $self->_update_land_tables($plant);
796 0           $self->_crop_yield_and_losses($tmp_quality);
797              
798 0           return;
799             }
800              
801             sub _update_land_tables {
802 0     0     my $self = shift;
803 0           my $plant = shift;
804              
805 0           my $valid = 0;
806              
807 0           my %tmp_quality = (
808             100 => 0,
809             80 => 0,
810             60 => 0,
811             40 => 0,
812             20 => 0,
813             0 => 0,
814             );
815              
816 0           my $quality = $self->land_fertility;
817              
818 0           my $qfactor;
819 0           for (qw( 100 80 60 40 20 0 )) {
820 0           $qfactor = $_;
821 0 0         if ( $plant <= $quality->{$qfactor} ) {
822 0           $valid = 1;
823 0           last;
824             }
825             else {
826 0           $plant -= $quality->{$qfactor};
827 0           $tmp_quality{$qfactor} = $quality->{$qfactor};
828 0           $quality->{$qfactor} = 0;
829             }
830             }
831              
832 0 0         if ( !$valid ) {
833 0           warn "LAND TABLE UPDATING ERROR - PLEASE CONTACT PROGRAM AUTHOR\n";
834 0           warn "IF ERROR IS NOT A FAULT OF ENTERING THE PROGRAM, BUT RATHER\n";
835 0           warn "FAULT OF THE PROGRAM LOGIC.\n";
836              
837 0           exit(1);
838             }
839              
840 0           $tmp_quality{$qfactor} = $plant;
841 0           $quality->{$qfactor} -= $plant;
842 0           $quality->{100} += $quality->{80};
843 0           $quality->{80} = 0;
844              
845 0           for ( 60, 40, 20, 0 ) {
846 0           $quality->{ $_ + 40 } += $quality->{$_};
847 0           $quality->{$_} = 0;
848             }
849              
850 0           for ( 100, 80, 60, 40, 20 ) {
851 0           $quality->{ $_ - 20 } += $tmp_quality{$_};
852             }
853              
854 0           $quality->{0} += $tmp_quality{0};
855              
856 0           return \%tmp_quality;
857             }
858              
859             sub _crop_yield_and_losses {
860 0     0     my $self = shift;
861 0           my $tmp_q = shift;
862              
863 0           $self->{_msg} = '';
864              
865 0           $self->{yield} = $self->_randomize('yield') + 3;
866 0 0         if ( !( $self->year % 7 ) ) {
867 0           $self->{_msg} .= "Seven year locusts\n";
868 0           $self->{yield} /= 2;
869             }
870              
871 0           my $x1 = 0;
872 0           for ( 100, 80, 60, 40, 20 ) {
873 0           $x1 += $tmp_q->{$_} * ( $_ / 100 );
874             }
875              
876 0           my $grain = $self->_grain;
877              
878 0 0         if ( $grain->yield == 0 ) {
879 0           $self->{yield} = 0;
880             }
881             else {
882 0           $self->{yield} =
883             int( $self->yield * ( $x1 / $grain->yield ) * 100 ) / 100;
884             }
885 0           $self->{_msg} .= sprintf( "Yield = %0.2f HL./HA.\n", $self->yield );
886              
887 0           $x1 = $self->_randomize('spoilage') + 3;
888 0 0         unless ( $x1 < 9 ) {
889 0           $grain->{spoilage} = -int( ( $x1 * $self->grain ) / 83 );
890 0           $self->{grain} += $grain->{spoilage};
891 0           $self->{_msg} .= "Rats infest the grainery\n";
892             }
893              
894 0           $self->_next_step('_display_msg');
895              
896 0           return;
897             }
898              
899             sub _kings_levy {
900 0     0     my $self = shift;
901              
902 0 0 0       return if ( $self->population < 67 ) || ( $self->king_unrest == -1 );
903              
904             # there is an edge case where entering an invalid answer might allow
905             # one to avoid this, but ... who cares
906 0           my $x1 = $self->_randomize('levies');
907 0 0         return if $x1 > ( $self->population / 30 );
908              
909 0           my $msg = sprintf( "The High King requires %d peasants for his estates\n",
910             int($x1) );
911 0           $msg .= sprintf( "and mines. Will you supply them or pay %d\n",
912             int( $x1 * 100 ) );
913 0           $msg .= "HL. of grain instead [Y/n]: ";
914              
915 0 0 0       $self->_next_step('_kings_levy')
916             and $self->throw( display => $msg, action => 'get_yn', default => 'Y' )
917             unless $self->input_is_yn();
918              
919 0 0         if ( $self->input =~ /^n/i ) {
920 0           $self->_grain->{taxes} = -100 * $x1;
921 0           $self->{grain} += $self->_grain->{taxes};
922             }
923             else {
924 0           $self->_population->{levy} = -int($x1);
925 0           $self->{population} += $self->_population->{levy};
926             }
927              
928 0           return;
929             }
930              
931             # TODO: find names for the "magic numbers" and change them to constants
932             sub _war_with_neigbor {
933 0     0     my $self = shift;
934              
935 0 0         if ( $self->king_unrest == -1 ) {
936 0           $self->{_msg} = "\nThe High King calls for peasant levies\n";
937 0           $self->{_msg} .= "and hires many foreign mercenaries\n";
938              
939 0           $self->{king_unrest} = -2;
940             }
941             else {
942 0           my $war = $self->_war;
943 0           $war->{potential} = int( 11 - ( 1.5 * $self->yield ) );
944 0 0         $war->{potential} = 2 if ( $war->potential < 2 );
945 0 0 0       if ( $self->king_unrest
      0        
946             || ( $self->population <= 109 )
947             || ( ( 17 * ( $self->land - 400 ) + $self->grain ) <= 10600 ) )
948             {
949 0           $war->{desire} = 0;
950             }
951             else {
952 0           $self->{_msg} = "\nThe High King grows uneasy and may\n";
953 0           $self->{_msg} .= "be subsidizing wars against you\n";
954              
955 0           $war->{potential} += 2;
956 0           $war->{desire} = $self->year + 5;
957             }
958 0           $self->{war} = int( $self->_randomize('war') );
959 0 0         $self->_next_step('_first_strike')
960             unless $self->war > $war->potential;
961 0           $self->_war->{first_strike} =
962             int(
963             $war->{desire} + 85 + ( 18 * $self->_randomize('first_strike') ) );
964             }
965 0 0         $self->_next_step('_display_msg') if $self->_msg;
966              
967 0           return;
968             }
969              
970             sub _first_strike {
971 0     0     my $self = shift;
972              
973 0           $self->_war->{will} = 1.2 - ( $self->_unrest / 16 );
974 0           my $x5 = int( $self->population * $self->_war->will ) + 13;
975              
976 0           my $msg = "A nearby Duke threatens war; Will you attack first [y/N]? ";
977              
978 0 0 0       $self->_next_step('_first_strike')
979             and $self->throw( display => $msg, action => 'get_yn', default => 'N' )
980             unless $self->input_is_yn();
981              
982 0           my $population = $self->_population;
983              
984 0           $self->{_msg} = '';
985 0 0         if ( $self->input !~ /^n/i ) {
986 0 0         if ( $self->_war->{first_strike} >= $x5 ) {
987 0           $self->_next_step('_goto_war');
988 0           $self->{_msg} = "First strike failed - you need professionals\n";
989 0           $population->{casualties} =
990             -$self->war - $self->_war->potential - 2;
991 0           $self->_war->{first_strike} += ( 3 * $population->casualties );
992             }
993             else {
994 0           $self->{_msg} = "Peace negotiations were successful\n";
995              
996 0           $population->{casualties} = -$self->_war->potential - 1;
997 0           $self->_war->{first_strike} = 0;
998             }
999 0           $self->{population} += $population->casualties;
1000 0 0         if ( $self->_war->first_strike < 1 ) {
1001 0           $self->{_unrest} -=
1002             ( 2 * $population->casualties ) + ( 3 * $population->looted );
1003             }
1004             }
1005             else {
1006 0           $self->_next_step('_goto_war');
1007             }
1008 0 0         $self->_next_step('_display_msg') if $self->_msg;
1009              
1010 0           return;
1011             }
1012              
1013             sub _goto_war {
1014 0     0     my $self = shift;
1015              
1016 0           my $possible = int( $self->grain / 40 );
1017 0 0         $possible = 75 if $possible > 75;
1018 0 0         $possible = 0 if $possible < 0;
1019              
1020 0           my $msg = "Hire how many mercenaries at 40 HL each [$possible]? ";
1021 0 0 0       $self->_next_step('_goto_war')
1022             and $self->throw(
1023             display => $msg,
1024             action => 'get_value',
1025             default => $possible
1026             ) unless $self->input_is_value();
1027              
1028 0   0       my $hired = $self->input || $possible;
1029              
1030 0 0         if ( $hired > 75 ) {
1031 0           my $msg = "There are only 75 mercenaries available for hire\n";
1032 0           $self->_next_step('_goto_war');
1033              
1034 0           $self->throw( display => $msg );
1035             }
1036              
1037 0           my $war = $self->_war;
1038 0           my $land = $self->_land;
1039              
1040 0           my $x5 = int( ( $self->population * $war->will ) + ( 7 * $hired ) + 13 );
1041              
1042 0           $war->{desire} = int( $war->desire * WAR_CONSTANT );
1043 0           print "General unrest: $self->{unrest}";
1044 0   0       print ' annual: ' . ( $self->{_unrest} || 'undef' );
1045 0           print " desire: $war->{desire}\n";
1046 0           print "will to fight: $war->{will}";
1047 0 0         print " x5: $x5\n"
1048             if $DEBUG;
1049              
1050 0           my $x6 = $war->desire - ( 4 * $hired ) - int( $x5 / 4 );
1051 0           $war->{desire} = $x5 - $war->desire;
1052 0           $land->{spoils} = int( 0.8 * $war->desire );
1053 0 0         if ( -$land->spoils > int( 0.67 * $self->land ) ) {
1054 0           $self->{_steps} = [];
1055 0           $self->_set_status(ABOLISHED);
1056              
1057 0           my $msg = "You have been overrun and have lost the entire Dukedom\n";
1058 0           $msg .= "The placement of your head atop the castle gate\n";
1059 0           $msg .= "signifies that ";
1060 0           $msg .= "the High King has abolished your Ducal right\n\n";
1061              
1062 0           $self->throw( display => $msg );
1063             }
1064              
1065 0           my $x1 = $land->spoils;
1066              
1067 0           my $fertility = $self->land_fertility;
1068 0           for ( 100, 80, 60 ) {
1069 0           my $x3 = int( $x1 / ( 3 - ( 5 - ( $_ / 20 ) ) ) );
1070 0 0         if ( -$x3 <= $fertility->{$_} ) {
1071 0           $x5 = $x3;
1072             }
1073             else {
1074 0           $x5 = -$fertility->{$_};
1075             }
1076 0           $fertility->{$_} += $x5;
1077 0           $x1 = $x1 - $x5;
1078             }
1079 0           for ( 40, 20, 0 ) {
1080 0 0         if ( -$x1 <= $fertility->{$_} ) {
1081 0           $x5 = $x1;
1082             }
1083             else {
1084 0           $x5 = -$fertility->{$_};
1085             }
1086 0           $fertility->{$_} += $x5;
1087 0           $x1 = $x1 - $x5;
1088             }
1089              
1090 0           my $grain = $self->_grain;
1091              
1092 0           $msg = '';
1093 0 0         if ( $land->spoils < 399 ) {
1094 0 0         if ( $war->desire >= 0 ) {
1095 0           $msg = "You have won the war\n";
1096 0           $war->{grain_damage} = 0.67;
1097 0           $grain->{spoils} = int( 1.7 * $land->spoils );
1098 0           $self->grain += $grain->spoils;
1099             }
1100             else {
1101 0           $msg = "You have lost the war\n";
1102 0           $war->{grain_damage} =
1103             int( ( $grain->yield / $self->land ) * 100 ) / 100;
1104             }
1105 0 0         if ( $x6 <= 9 ) {
1106 0           $x6 = 0;
1107             }
1108             else {
1109 0           $x6 = int( $x6 / 10 );
1110             }
1111             }
1112             else {
1113 0           $msg = "You have overrun the enemy and annexed his entire Dukedom\n";
1114              
1115 0           $grain->{spoils} = 3513;
1116 0           $self->{grain} += $grain->spoils;
1117 0           $x6 = -47;
1118 0           $war->{grain_damage} = 0.55;
1119 0 0         if ( $self->king_unrest <= 0 ) {
1120 0           $self->{king_unrest} = 1;
1121 0           $msg .= "The King fears for his throne and\n";
1122 0           $msg .= "may be planning direct action\n";
1123             }
1124             }
1125              
1126 0 0         $x6 = $self->population if ( $x6 > $self->population );
1127              
1128 0           my $population = $self->_population;
1129              
1130 0           $population->{casualties} -= $x6;
1131 0           $self->{population} -= $x6;
1132 0           $grain->{yield} += int( $war->grain_damage * $land->spoils );
1133 0           $x6 = 40 * $hired;
1134 0 0         if ( $x6 <= $self->grain ) {
1135 0           $grain->{wages} = -$x6;
1136              
1137             # what is P[5] (looted) in this case?
1138             }
1139             else {
1140 0           $grain->{wages} = -$self->grain;
1141 0           $population->{looted} = -int( ( $x6 - $self->grain ) / 7 ) - 1;
1142 0           $msg .= "There isn't enough grain to pay the mercenaries\n";
1143             }
1144 0           $self->{grain} += $grain->wages;
1145              
1146 0 0         --$self->{population} if ( -$population->looted > $self->population );
1147              
1148 0           $self->{population} += $population->looted;
1149 0           $self->{land} += $land->spoils;
1150 0           $self->{_unrest} -=
1151             ( 2 * $population->casualties ) - ( 3 * $population->looted );
1152              
1153 0 0         $self->_next_step('_display_msg') if $self->{_msg} = $msg;
1154              
1155 0           return;
1156             }
1157              
1158             sub _population_changes {
1159 0     0     my $self = shift;
1160              
1161 0           my $x1 = $self->_randomize('disease');
1162              
1163 0           my $population = $self->_population;
1164 0           my $x2;
1165 0 0         if ( $x1 <= 3 ) {
1166 0 0         if ( $x1 != 1 ) {
    0          
1167 0           $self->{_msg} = "A POX EPIDEMIC has broken out\n";
1168 0           $self->_next_step('_display_msg');
1169              
1170 0           $x2 = $x1 * 5;
1171 0           $population->{diseased} = -int( $self->population / $x2 );
1172 0           $self->{population} += $population->diseased;
1173             }
1174             elsif ( $self->black_D <= 0 ) {
1175 0           $self->{_msg} = "The BLACK PLAGUE has struck the area\n";
1176 0           $self->_next_step('_display_msg');
1177              
1178 0           $self->{black_D} = 13;
1179 0           $x2 = 3;
1180 0           $population->{diseased} = -int( $self->population / $x2 );
1181 0           $self->{population} += $population->diseased;
1182             }
1183             }
1184              
1185 0 0         $x1 = $population->looted ? 4.5 : $self->_randomize('birth') + 4;
1186              
1187 0           $population->{births} = int( $self->population / $x1 );
1188 0           $population->{deaths} = int( 0.3 - ( $self->population / 22 ) );
1189 0           $self->{population} += $population->deaths + $population->births;
1190              
1191 0           --$self->{black_D};
1192              
1193 0           return;
1194             }
1195              
1196             sub _harvest_grain {
1197 0     0     my $self = shift;
1198              
1199 0           my $grain = $self->_grain;
1200              
1201 0           $grain->{yield} = int( $self->yield * $grain->yield );
1202 0           $self->{grain} += $grain->yield;
1203              
1204 0           my $x1 = $grain->yield - 4000;
1205 0 0         $grain->{expense} = -int( 0.1 * $x1 ) if $x1 > 0;
1206              
1207 0           $grain->{expense} -= MIN_EXPENSE;
1208 0           $self->{grain} += $grain->expense;
1209              
1210             # you've already told the King what to do with his taxes, he's coming
1211             # to collect them (and more) in person now.
1212 0 0         return if $self->king_unrest < 0;
1213              
1214 0 0         my $tax_rate = $self->king_unrest >= 2 ? TAX_RATE * 2 : TAX_RATE;
1215 0           $x1 = -int( $self->land * $tax_rate );
1216              
1217 0 0         if ( -$x1 > $self->grain ) {
1218 0           $self->{_msg} = "You have insufficient grain to pay the royal tax\n";
1219 0           $self->{_msg} .= "the High King has abolished your Ducal right\n\n";
1220 0           $self->_next_step('_display_msg');
1221              
1222 0           $self->_set_status(ABOLISHED);
1223 0           return 1;
1224             }
1225 0           $grain->{taxes} += $x1;
1226 0           $self->{grain} += $x1;
1227              
1228 0           return;
1229             }
1230              
1231             sub _update_unrest {
1232 0     0     my $self = shift;
1233              
1234 0           $self->{unrest} = int( $self->unrest * UNREST_FACTOR ) + $self->_unrest;
1235              
1236 0           return;
1237             }
1238              
1239             sub _quit_game {
1240 0     0     my $self = shift;
1241              
1242 0           $self->_set_status(QUIT_GAME);
1243 0           $self->{_steps} = [];
1244              
1245 0           return;
1246             }
1247              
1248             sub _end_of_game_check {
1249 0     0     my $self = shift;
1250              
1251 0           my $msg = '';
1252              
1253 0 0 0       if ( $self->status eq QUIT_GAME ) {
    0 0        
    0 0        
    0          
    0          
1254 0           $msg = "\nYou have conceded the game\n\n";
1255             }
1256             elsif (( $self->grain < MIN_GRAIN )
1257             || ( $self->_unrest > MAX_1YEAR_UNREST )
1258             || ( $self->unrest > MAX_TOTAL_UNREST ) )
1259             {
1260 0           $msg = "\nThe peasants tire of war and starvation\n";
1261 0           $msg .= "You are deposed!\n\n";
1262              
1263 0 0         print "General unrest: $self->{unrest} annual: $self->{_unrest}\n"
1264             if $DEBUG;
1265              
1266 0           $self->_set_status(DEPOSED);
1267             }
1268             elsif ( $self->population < MIN_POPULATION ) {
1269 0           $msg = "You have so few peasants left that\n";
1270 0           $msg .= "the High King has abolished your Ducal right\n\n";
1271              
1272 0           $self->_set_status('ABOLISHED');
1273             }
1274             elsif ( $self->land < MIN_LAND ) {
1275 0           $msg = "You have so little land left that\n";
1276 0           $msg .= "the High King has abolished your Ducal right\n\n";
1277              
1278 0           $self->_set_status(ABOLISHED);
1279             }
1280             elsif ( $self->year >= MAX_YEAR && !$self->king_unrest ) {
1281 0           $msg = "You have reached the age of mandatory retirement\n\n";
1282              
1283 0           $self->_set_status(RETIRED);
1284             }
1285              
1286 0 0         $self->throw( display => $msg ) if $self->game_over;
1287              
1288 0           return;
1289             }
1290              
1291             sub _insufficient_grain {
1292 0     0     my $self = shift;
1293 0           my $action = shift;
1294              
1295 0           my %msgs = (
1296             feed => sprintf( "You have %d HL. of grain left,\n", $self->grain ),
1297             buy => sprintf( "Enough to buy %d HA. of land\n",
1298             int( $self->grain / $self->_land->{price} ) ),
1299             plant => sprintf( "Enough to plant %d HA. of land\n\n",
1300             int( $self->grain / 2 ) ),
1301             );
1302              
1303 0           my $msg = "You don't have enough grain\n";
1304 0           $msg .= $msgs{$action};
1305              
1306 0           return $msg;
1307             }
1308              
1309             sub end_of_year_report {
1310 0     0 1   my $self = shift;
1311              
1312 0           my $msg = "\n";
1313 0           for ( sort( keys( %{ $self->_population } ) ) ) {
  0            
1314 0           $msg .= sprintf( "%-20.20s %d\n", $_, $self->_population->$_ );
1315             }
1316 0           $msg .= sprintf( "%-20.20s %d\n\n", "Peasants at end", $self->population );
1317              
1318 0           for ( sort( keys( %{ $self->_land } ) ) ) {
  0            
1319 0           $msg .= sprintf( "%-20.20s %d\n", $_, $self->_land->$_ );
1320             }
1321 0           $msg .= sprintf( "%-20.20s %d\n\n", "Land at end", $self->land );
1322              
1323 0           for ( sort( keys( %{ $self->_grain } ) ) ) {
  0            
1324 0           $msg .= sprintf( "%-20.20s %d\n", $_, $self->_grain->$_ );
1325             }
1326 0           $msg .= sprintf( "%-20.20s %d\n\n", "Grain at end", $self->grain );
1327              
1328 0           for ( sort( keys( %{ $self->_war } ) ) ) {
  0            
1329 0           $msg .= sprintf( "%-20.20s %d\n", $_, $self->_war->$_ );
1330             }
1331 0           $msg .= sprintf( "%-20.20s %d\n\n", "War factor", $self->war );
1332              
1333 0           return $msg;
1334             }
1335              
1336             1;
1337              
1338             __END__
1339              
1340             =head1 NAME
1341              
1342             Games::Dukedom - The classic big-iron game
1343              
1344             =head1 SYNOPSIS
1345              
1346            
1347             use Games::Dukedom;
1348            
1349             my $game = Games::Dukedom->new();
1350            
1351              
1352             =head1 DESCRIPTION
1353              
1354             This is an implementation of the classic game of "Dukedom". It is intended
1355             to be display agnostic so that it can be used not only by command line
1356             scripts such as the one included but also by graphical UIs such as Tk
1357             or web sites.
1358              
1359             It has been implemented as an "interrupt driven" state-machine. The actual
1360             executable only needs to provide the callback hooks to display messages
1361             and collect appropriate input as requested.
1362              
1363             =head1 CONSTRUCTOR
1364              
1365             One begins the game by calling the expected C<new> method like so:
1366              
1367            
1368             my $game = Games::Dukedom->new();
1369            
1370            
1371             It currently does not take any parameters.
1372              
1373             =head1 ACCESSORS
1374              
1375             =head2 black_D
1376              
1377             =head2 grain
1378              
1379             =head2 input
1380              
1381             =head2 king_unrest
1382              
1383             =head2 land
1384              
1385             =head2 land_fertility
1386              
1387             =head2 population
1388              
1389             =head2 status
1390              
1391             =head2 unrest
1392              
1393             =head2 war
1394              
1395             =head2 year
1396              
1397             =head2 yield
1398              
1399             =head1 METHODS
1400              
1401             =head2 play_one_year
1402              
1403             This method begins a new year of play. It initializes the temporary structures
1404             and factors and resets the state-machine.
1405              
1406             =head2 game_over
1407              
1408             Indicates that current game is over and further play is not possible.
1409             A "win" is indicated by a positive value, a "loss" by a negative one.
1410              
1411             =over 4
1412              
1413             =item 2 - It's GOOD to be the King!
1414              
1415             =item 1 - You have retired
1416              
1417             =item -1 - You have been deposed
1418              
1419             =item -2 - Don't mess with the King!
1420              
1421             =item -3 - You have abandoned the game
1422              
1423             =back
1424              
1425             =head2 input_is_yn
1426              
1427             Returns a boolean indicating that the current contents of C<< $self->input >>
1428             is either "Y" or "N" (case insensitive).
1429              
1430             =head2 input_is_value
1431              
1432             Returns a boolean indicating that the current contents of C<< $self->input >>
1433             is "0" or a positive integer.
1434              
1435             =head2 summary_report
1436              
1437             =head2 fertility_report
1438              
1439             =head2 end_of_year_report
1440              
1441             =head1 SEE ALSO
1442              
1443             This package is based on the logic found in this C code, which appears to
1444             have been derived from an older source written in Basic:
1445              
1446             L<https://github.com/caryo/Dukedom/blob/master/imports/dukedom.c>
1447              
1448             =head1 BUGS
1449              
1450             Seriously? Look at the version number.
1451              
1452             =head1 AUTHOR
1453              
1454             Jim Bacon, E<lt>jim@nortx.comE<gt>
1455              
1456             =head1 COPYRIGHT AND LICENSE
1457              
1458             Copyright (C) 2014 by Jim Bacon
1459              
1460             This library is free software; you can redistribute it and/or modify it
1461             under the same terms as Perl itself, either Perl version or,
1462             at your option, any later version of Perl 5 you may have available.
1463              
1464             =cut