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