Reindent code, tests, Makefile.PL
[mafia.git] / lib / Mafia.pm
CommitLineData
f1171346
MG
1package Mafia;
2
3use 5.014000;
4use strict;
5use warnings;
6use parent qw/Exporter/;
7
8use constant;
9use Storable qw/dclone/;
10
11our $VERSION = '0.001';
12
13sub defconst { constant->import($_ => $_) for @_ }
14
15BEGIN {
40af1ba2
MG
16 # Roles
17 defconst qw/vanilla goon doctor vigilante roleblocker jailkeeper gunsmith tracker watcher bodyguard rolecop cop sk hider/;
f1171346 18
40af1ba2
MG
19 # Factions
20 defconst qw/mafia town/;
f1171346 21
40af1ba2
MG
22 # Extra traits
23 defconst qw/miller godfather weak macho bulletproof/;
f1171346 24
40af1ba2
MG
25 # Messages
26 defconst qw/MSG_NIGHT MSG_DAY MSG_PLAYERS_ALIVE MSG_DEATH MSG_GUNCHECK MSG_NORESULT MSG_TRACK MSG_WATCH MSG_COP MSG_ROLECOP/;
f1171346 27
40af1ba2
MG
28 # Action types
29 defconst qw/ACT_KILL ACT_LYNCH ACT_PROTECT ACT_GUARD ACT_ROLEBLOCK ACT_GUNCHECK ACT_TRACK ACT_WATCH ACT_ROLECOP ACT_COP ACT_TRACK_RESULT ACT_WATCH_RESULT ACT_HIDE/;
f1171346
MG
30}
31
32use constant +{
40af1ba2
MG
33 townie => town,
34 ROLE => [vanilla, goon, doctor, vigilante, roleblocker, jailkeeper, gunsmith, tracker, watcher, bodyguard, rolecop, cop, sk, hider],
35 FACTION => [mafia, town],
36 FLAG => [miller, godfather, weak, macho, bulletproof],
37 ACTION_ORDER => [ACT_HIDE, ACT_ROLEBLOCK, ACT_PROTECT, ACT_GUARD, ACT_GUNCHECK, ACT_ROLECOP, ACT_COP, ACT_TRACK, ACT_WATCH, ACT_KILL, ACT_LYNCH, ACT_TRACK_RESULT, ACT_WATCH_RESULT],
38 INVESTIGATIVE_ACTIONS => [ACT_GUNCHECK, ACT_TRACK, ACT_WATCH, ACT_ROLECOP, ACT_COP],
39 GUNROLES => [vigilante, gunsmith],
f1171346
MG
40};
41
42our @EXPORT = do {
40af1ba2
MG
43 no strict 'refs';
44 grep { $_ !~ [qw/import/] and exists &$_ } keys %{__PACKAGE__ . '::'};
f1171346
MG
45};
46
47################################################## Helper subs
48
49sub import {
40af1ba2
MG
50 strict->import;
51 goto &Exporter::import;
f1171346
MG
52}
53
54my (%players, %tplayers, @actions);
55my $daycnt = 0;
56my $nightcnt = 0;
57my $isday = 0;
58my $first = 1;
59
60sub clean{
40af1ba2
MG
61 %players = ();
62 %tplayers = ();
63 @actions = ();
64 $daycnt = 0;
65 $nightcnt = 0;
66 $isday = 0;
67 $first = 1;
f1171346
MG
68}
69
70sub uniq {
40af1ba2
MG
71 my %hash = map { $_ => 1 } @_;
72 keys %hash
f1171346
MG
73}
74
75sub phase {
40af1ba2
MG
76 return "Day $daycnt" if $isday;
77 return "Night $nightcnt" unless $isday;
f1171346
MG
78}
79
80sub rolename {
40af1ba2
MG
81 my %player = %{$players{$_[0]}};
82 my ($faction, $role) = ($player{faction}, $player{role});
83 if (defined $faction && $faction eq town && $role eq vanilla) {
84 undef $faction;
85 $role = 'Vanilla Townie';
f1171346 86 }
40af1ba2
MG
87 my @tokens = ();
88 push @tokens, ucfirst $faction if $faction;
89 for my $flag (@{FLAG()}) {
90 push @tokens, ucfirst $flag if $player{$flag}
f1171346 91 }
40af1ba2
MG
92 push @tokens, ucfirst $role unless $role eq goon && $player{godfather};
93 "@tokens"
94}
f1171346 95
40af1ba2
MG
96sub msg {
97 my ($type, @args) = @_;
98 given ($type){
99 when (MSG_NIGHT) {
100 my ($night) = @args;
101 say '' unless $first;
102 $first = 0;
103 say "It is Night $night";
104 }
105
106 when (MSG_DAY) {
107 my ($day) = @args;
108 say '' unless $first;
109 $first = 0;
110 say "It is Day $day";
111 }
112
113 when (MSG_PLAYERS_ALIVE) {
114 @args = sort @args;
115 say "Players alive: ", join ', ', @args
116 }
117
118 when (MSG_DEATH) {
119 my %args = @args;
120 my ($who, $reason) = @args{'target', 'reason'};
121 my $phase = phase;
122 my $rolename = rolename $who;
123 say "$who ($rolename) — $reason $phase";
124 }
125
126 when (MSG_GUNCHECK) {
127 my %args = @args;
128 my ($gunsmith, $who, $hasgun) = @args{'source', 'target', 'result'};
129 say "$gunsmith: $who has a gun" if $hasgun;
130 say "$gunsmith: $who does not have a gun" unless $hasgun;
131 }
132
133 when (MSG_NORESULT) {
134 my %args = @args;
135 my ($who) = $args{'source'};
136 say "$who: No result"
137 }
138
139 when (MSG_TRACK) {
140 my %args = @args;
141 my ($tracker, $who, $result) = @args{'source', 'target', 'result'};
142 my @result = @{$result};
143 local $, = ', ';
144 say "$tracker: $who did not visit anyone" unless scalar @result;
145 say "$tracker: $who visited: @result" if scalar @result;
146 }
147
148 when (MSG_WATCH) {
149 my %args = @args;
150 my ($watcher, $who, $result) = @args{'source', 'target', 'result'};
151 my @result = @{$result};
152 local $, = ', ';
153 say "$watcher: $who was not visited by anyone" unless scalar @result;
154 say "$watcher: $who was visited by: @result" if scalar @result;
155 }
156
157 when (MSG_ROLECOP) {
158 my %args = @args;
159 my ($rolecop, $who, $role) = @args{'source', 'target', 'result'};
160 say "$rolecop: $who\'s role is: $role"
161 }
162
163 when (MSG_COP) {
164 my %args = @args;
165 my ($cop, $who, $ismafia) = @args{'source', 'target', 'result'};
166 say "$cop: $who is mafia" if $ismafia;
167 say "$cop: $who is not mafia" unless $ismafia;
168 }
f1171346 169 }
f1171346
MG
170}
171
172sub putaction {
40af1ba2
MG
173 my ($delay, $type, %args) = @_;
174 $actions[$delay]->{$type} //= [];
175 if (exists $args{target} && exists $args{source} && $players{$args{target}}{faction} eq mafia && $players{$args{source}}{weak}) {
176 putaction($delay, ACT_KILL, target => $args{source}, reason => 'targeted scum');
177 }
178 push $actions[$delay]->{$type}, \%args
f1171346
MG
179}
180
181sub doaction {
40af1ba2
MG
182 my ($type, $args) = @_;
183 my %args = %$args;
184 my $source = $args{source};
185 my $target = $args{target};
186 if (defined $source && defined $target) {
187 # Watcher and tracker variables
188 $tplayers{$source}{targets} //= [];
189 push $tplayers{$source}{targets}, $target;
190 $tplayers{$target}{sources} //= [];
191 push $tplayers{$target}{sources}, $source;
192
193 # Copy this action to everybody hiding behind $target
194 if (exists $tplayers{$target}{hiders}) {
195 for my $target (@{$tplayers{$target}{hiders}}) {
196 my %args = %args;
197 $args{target} = $target;
198 $args{hidepierce} = 1;
199 doaction($type, \%args);
200 }
201 }
202
203 # Check if the action should be blocked
204 my $strongkill = $type eq ACT_KILL && $args{strong};
205 my $roleblocked = $tplayers{$source}{roleblocked};
206 my $hidden = $tplayers{$target}{hidden};
207 my $hidepierce = $args{hidepierce};
208 if ($source && (( $roleblocked && !$strongkill ) || ($hidden && !$hidepierce) )) {
209 msg MSG_NORESULT, %args if $type ~~ INVESTIGATIVE_ACTIONS;
210 return
211 }
f1171346
MG
212 }
213
f1171346 214
40af1ba2
MG
215 given ($type) {
216 when(ACT_KILL) {
217 break if $tplayers{$target}{bulletproof} && defined $source;
218 if ($tplayers{$target}{guard_count} && defined $source) {
219 $tplayers{$target}{guard_count}--;
220 # Copy this action to the first guard
221 $args{target} = shift $tplayers{$target}{guards};
222 @_ = ($type, %args);
223 goto &doaction;
224 }
225 if ($tplayers{$target}{protection} && !$args{strong}) {
226 $tplayers{$target}{protection}--;
227 break
228 }
229 msg MSG_DEATH, %args;
230 delete $players{$target}
231 }
232
233 when(ACT_LYNCH){
234 if ($tplayers{$target}{guard_count}) {
235 $tplayers{$target}{guard_count}--;
236 $args{target} = shift $tplayers{$target}{guards};
237 $target=$args{target};
238 }
239 if ($tplayers{$target}{protection}) {
240 $tplayers{$target}{protection}--;
241 break
242 }
243 msg MSG_DEATH, %args, reason => 'lynched';
244 delete $players{$target}
245 }
246
247 when(ACT_PROTECT){
248 my $count = $args{count} // 1;
249 $tplayers{$target}{protection} += $count unless $tplayers{$target}{macho}
250 }
251
252 when(ACT_ROLEBLOCK){
253 $tplayers{$target}{roleblocked} = 1
254 }
255
256 when(ACT_GUNCHECK){
257 my $role = $players{$target}{role};
258 my $hasgun = $role ~~ GUNROLES || ($players{$target}{faction} eq mafia && $role ne doctor);
259 msg MSG_GUNCHECK, %args, result => $hasgun
260 }
261
262 when(ACT_TRACK_RESULT){
263 msg MSG_TRACK, %args, result => [ uniq @{$tplayers{$target}{targets} // []} ];
264 }
265
266 when(ACT_WATCH_RESULT){
267 msg MSG_WATCH, %args, result => [ uniq @{$tplayers{$target}{sources} // []} ];
268 }
269
270 when(ACT_GUARD){
271 $tplayers{$target}{guard_count}++;
272 $tplayers{$target}{guards} //= [];
273 push $tplayers{$target}{guards}, $source;
274 }
275
276 when(ACT_ROLECOP){
277 my $result = $players{$target}{role};
278 $result = vanilla if $result eq goon;
279 msg MSG_ROLECOP, %args, result => ucfirst $result
280 }
281
282 when(ACT_COP){
283 my $result = $players{$target}{faction} eq mafia;
284 $result = 1 if $players{$target}{miller};
285 $result = 0 if $players{$target}{godfather};
286 msg MSG_COP, %args, result => $result
287 }
288
289 when(ACT_HIDE){
290 $tplayers{$source}{hidden} = 1;
291 $tplayers{$target}{hiders} //= [];
292 push $tplayers{$target}{hiders}, $source
293 }
f1171346 294 }
f1171346
MG
295}
296
297sub process_phase_change {
40af1ba2
MG
298 %tplayers = %{dclone \%players};
299 my $actions = shift @actions;
300 for my $type (@{ACTION_ORDER()}) {
301 doaction $type, $_ for @{$actions->{$type}}
302 }
f1171346
MG
303}
304
305################################################## User subs
306
307sub player {
40af1ba2
MG
308 my ($name, @args) = @_;
309 my %player;
310 for my $trait (@args) {
311 given ($trait) {
312 $player{role} = $trait when ROLE;
313 $player{faction} = $trait when FACTION;
314 $player{$trait} = 1 when FLAG;
315 }
f1171346 316 }
f1171346 317
40af1ba2 318 $players{$name} = \%player;
f1171346
MG
319}
320
321sub day {
40af1ba2
MG
322 process_phase_change;
323 $isday = 1;
324 msg MSG_DAY, ++$daycnt;
325 msg MSG_PLAYERS_ALIVE, keys %players;
f1171346
MG
326}
327
328sub night {
40af1ba2
MG
329 process_phase_change;
330 $isday = 0;
331 msg MSG_NIGHT, ++$nightcnt;
332 msg MSG_PLAYERS_ALIVE, keys %players;
f1171346
MG
333}
334
335sub lynch {
40af1ba2
MG
336 my ($who) = @_;
337 putaction 0, ACT_LYNCH, target => $who;
f1171346
MG
338}
339
340sub factionkill {
40af1ba2
MG
341 my ($killer, $who, $reason, @args) = @_;
342 putaction 0, ACT_KILL, target => $who, source => $killer, reason => $reason, @args;
f1171346
MG
343}
344
345sub protect {
40af1ba2
MG
346 my ($doctor, $who) = @_;
347 putaction 0, ACT_PROTECT, target => $who, source => $doctor;
f1171346
MG
348}
349
350sub vig {
40af1ba2
MG
351 my ($vig, $who, $reason, @args) = @_;
352 putaction 0, ACT_KILL, target => $who, source => $vig, reason => $reason, @args;
f1171346
MG
353}
354
355sub roleblock {
40af1ba2
MG
356 my ($roleblocker, $who) = @_;
357 putaction 0, ACT_ROLEBLOCK, target => $who, source => $roleblocker;
f1171346
MG
358}
359
360sub jailkeep {
40af1ba2
MG
361 my ($jailkeeper, $who) = @_;
362 putaction 0, ACT_ROLEBLOCK, target => $who, source => $jailkeeper;
363 putaction 0, ACT_PROTECT, target => $who, source => $jailkeeper, count => 1000;
f1171346
MG
364}
365
366sub guncheck {
40af1ba2
MG
367 my ($gunsmith, $who) = @_;
368 putaction 0, ACT_GUNCHECK, target => $who, source => $gunsmith;
f1171346
MG
369}
370
371sub track {
40af1ba2
MG
372 my ($tracker, $who) = @_;
373 putaction 0, ACT_TRACK, target => $who, source => $tracker;
374 putaction 0, ACT_TRACK_RESULT, target => $who, source => $tracker;
f1171346
MG
375}
376
377sub watch {
40af1ba2
MG
378 my ($watcher, $who) = @_;
379 putaction 0, ACT_WATCH, target => $who, source => $watcher;
380 putaction 0, ACT_WATCH_RESULT, target => $who, source => $watcher;
f1171346
MG
381}
382
383sub guard {
40af1ba2
MG
384 my ($guard, $who) = @_;
385 putaction 0, ACT_GUARD, target => $who, source => $guard;
f1171346
MG
386}
387
388sub rolecopcheck {
40af1ba2
MG
389 my ($rolecop, $who) = @_;
390 putaction 0, ACT_ROLECOP, target => $who, source => $rolecop;
f1171346
MG
391}
392
393sub copcheck {
40af1ba2
MG
394 my ($cop, $who) = @_;
395 putaction 0, ACT_COP, target => $who, source => $cop;
f1171346
MG
396}
397
398sub skill {
40af1ba2
MG
399 my ($sk, $who, $reason, @args) = @_;
400 putaction 0, ACT_KILL, target => $who, source => $sk, reason => $reason, @args;
f1171346
MG
401}
402
403sub hide {
40af1ba2
MG
404 my ($hider, $who) = @_;
405 putaction 0, ACT_HIDE, target => $who, source => $hider;
f1171346
MG
406}
407
4081;
409__END__
410
86ad791b
MG
411=encoding utf-8
412
f1171346
MG
413=head1 NAME
414
415Mafia - easily moderate Mafia games
416
417=head1 SYNOPSIS
418
419 #!/usr/bin/perl -w
420 use Mafia;
421
422 player 'Banana Bob', cop, town;
423 player 'Dragon Phoenix', vanilla, townie;
424 player 'Gammie', mafia, goon;
425 player 'gslamm', vanilla, townie;
426 player 'Untrod Tripod', mafia, goon;
427 player 'Werebear', vanilla, townie;
428 player 'willows_weep', town, doctor;
429
430 day;
431 lynch 'Untrod Tripod';
432
433 night;
434 factionkill 'Gammie', 'willows_weep', 'shot';
435 copcheck 'Banana Bob', 'gslamm';
436
437 day;
438 lynch 'Gammie';
439
440 night;
441
442=head1 DESCRPTION
443
444Mafia.pm is a Perl extension for easily moderating Mafia games. You don't even need to know Perl to use it (see L<"WHAT YOU NEED TO KNOW"> for details).
445
446=head1 WHAT YOU NEED TO KNOW
447
448A typical script starts with the following two lines
449
450 #!/usr/bin/perl -w
451 use Mafia;
452
453The rest of the script is a series of function calls that describe the players and their actions.
454
455A function call looks like this:
456
457 function_name first_argument, second_argument, ...
458
459Each argument is either a number, a string (which is a sequence of characters between single or double quotes, such as C<'badguy'>, C<'qwrf'>) or a constant (such as C<mafia>, C<vanilla>, C<bulletproof>).
460
461Example calls:
462
463 player 'Somebody', mafia, goon; # player is the function, 'Somebody' is a string, mafia and goon are constants.
464 lynch 'Nobody'; # lynch is the function, 'Nobody' is a string.
465 day; # day is the function. There are no arguments.
466
467=head1 FUNCTIONS
468
469=over
470
471=item B<player> I<name>, I<trait>, ...
472
473Defines a new player named I<name> and its traits (role, faction, role modifiers).
474
475Roles: C<vanilla, goon, doctor, vigilante, roleblocker, jailkeeper, gunsmith, tracker, watcher, bodyguard, rolecop, cop, sk, hider>.
476
477Factions: C<mafia, town>. C<townie> is a synonim for C<town>.
478
479Other attributes: C<miller, godfather, weak, macho, bulletproof>
480
481These traits may be specified in any order.
482
483Example usage:
484
485 player 'alice', town, bulletproof, miller, vigilante; # Alice is a NK-Immune Miller Vig
486 player 'bob', town, weak, doctor; # Bob is a Town Weak Doctor
487 player 'eve', mafia, godfather, goon; # Eve is a Mafia Godfather
488
489=item B<day>
490
491Defines the start of a new Day. All actions in the previous Night are now resolved.
492
493=item B<night>
494
495Defines the start of a new Night. All actions in the previous Day are now resolved.
496
497=item B<lynch> I<player>
498
499Notes that I<player> was lynched.
500
501=item B<factionkill> I<killer>, I<player>, I<flavour>, [ strong => 1 ]
502
503Notes that I<killer> killed I<player> with flavour I<flavour>. Append C<< strong => 1 >> if the kill should ignore roleblocks and doctor/jailkeeper protections. Use this for mafia kills.
504
505Example usage:
506
507 factionkill 'eve', 'alice', 'strangled to death';
508 factionkill 'eve', 'bob', 'brutally murdered', strong => 1; # This is a strongman kill
509
510=item B<protect> I<doctor>, I<player>
511
512Notes that I<doctor> protected I<player>.
513
514=item B<vig> I<vigilante>, I<player>, I<flavour>, [ strong => 1 ]
515
516Notes that I<killer> killed I<player> with flavour I<flavour>. Append C<< strong => 1 >> if the kill should ignore roleblocks and doctor/jailkeeper protections. Use this for Vigilante/Juggernaut kills.
517
518Example usage:
519
520 vig 'chuck', 'bob', 'shot';
521 vig 'chuck', 'bob', 'shot seven times', strong => 1; # This is a Juggernaut (Strongman Vigilante) kill
522
523=item B<roleblock> I<roleblocker>, I<player>
524
525Notes that I<roleblocker> roleblocked I<player>.
526
527=item B<jailkeep> I<jailkeeper>, I<player>
528
529Notes that I<jailkeeper> roleblocked and protected I<player>.
530
531=item B<guncheck> I<gunsmith>, I<player>
532
533Notes that I<gunsmith> checked if I<player> has a gun.
534
535=item B<track> I<tracker>, I<player>
536
537Notes that I<tracker> tracked I<player>.
538
539=item B<watch> I<watcher>, I<player>
540
541Notes that I<watcher> watched I<player>.
542
543=item B<guard> I<bodyguard>, I<player>
544
545Notes that I<bodyguard> guarded I<player>
546
547=item B<rolecopcheck> I<rolecop>, I<player>
548
549Notes that I<rolecop> checked the role of I<player>
550
551=item B<copcheck> I<cop>, I<player>
552
553Notes that I<cop> checked whether I<player> is mafia.
554
555=item B<skill> I<SK>, I<player>, I<flavour>, [ strong => 1 ]
556
557Notes that I<SK> killed player with flavour I<flavour>. Append C<< strong => 1 >>> if the kill should ignore roleblocks and doctor/jailkeeper protections. Use this for Serial Killer kills.
558
559=item B<hide> I<hider>, I<player>
560
561Notes that I<hider> hid behind I<player>.
562
563=back
564
565=head1 AUTHOR
566
567Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
568
569=head1 COPYRIGHT AND LICENSE
570
571Copyright (C) 2013 by Marius Gavrilescu
572
573This library is free software; you can redistribute it and/or modify
574it under the same terms as Perl itself, either Perl version 5.14.2 or,
575at your option, any later version of Perl 5 you may have available.
576
577
578=cut
This page took 0.042197 seconds and 4 git commands to generate.