Support filtering by version in NetHack::NAOdash
[nethack-naodash.git] / lib / NetHack / NAOdash.pm
CommitLineData
01ba3ddc
MG
1package NetHack::NAOdash;
2
3use 5.014000;
4use strict;
5use warnings;
6use re '/saa';
7use parent qw/Exporter/;
8
90d9e1ee 9our $VERSION = '0.002';
01ba3ddc
MG
10our @EXPORT_OK = qw/naodash_xlog naodash_user/;
11our @EXPORT = @EXPORT_OK;
12
49bfce9d
MG
13use File::Slurp;
14use File::Spec::Functions qw/tmpdir catdir catfile/;
01ba3ddc
MG
15use HTTP::Tiny;
16use List::Util qw/max min sum/;
17use List::MoreUtils qw/uniq/;
18use Text::XLogfile qw/parse_xlogline/;
19
20sub won_game {
21 my %game = @_;
22 $game{death} eq 'ascended'
23}
24
25our @check_subs = (
26 sub { # Combos
27 my %game = @_;
28 return unless won_game %game;
29 $game{align0} //= $game{align};
30 "combo_$game{role}_$game{race}_$game{align0}"
31 },
32
33 sub { # Achievements
34 my %game = @_;
35 my @achieves = qw/bell gehennom candelabrum book invocation amulet endgame astral ascended luckstone sokoban medusa/;
36 map { $game{achieve} & (1 << $_) ? "achieve_$achieves[$_]" : () } 0 .. $#achieves
37 },
38
39 sub { # Conducts
40 my %game = @_;
41 return unless won_game %game;
42 my @conducts = qw/foodless vegan vegetarian atheist weaponless pacifist illiterate polypileless polyselfless wishless artiwishless genocideless/;
43 map { $game{conduct} & (1 << $_) ? "conduct_$conducts[$_]" : () } 0 .. $#conducts
44 },
45
46 sub { # Unofficial conducts
47 my %game = @_;
48 return unless won_game %game;
49 my @uconducts;
50 push @uconducts, 'survivor' if $game{deaths} == 0;
51 push @uconducts, 'boneless' unless $game{flags} & 32;
52 push @uconducts, 'minscore' if $game{points} - 100 * ($game{maxlvl} - 45) == 24_400;
53 map { "uconduct_$_" } @uconducts
54 },
55);
56
57our %sum_subs = (
58 games => sub { 1 },
59 ascensions => sub {
60 my %game = @_;
61 !!won_game %game
62 },
63 totalrealtime => sub {
64 my %game = @_;
65 $game{realtime} // 0
66 },
67);
68
69sub make_attr_sub ($) { ## no critic (ProhibitSubroutinePrototypes)
70 my ($attr) = @_;
71 sub {
72 my %game = @_;
73 return unless won_game %game;
74 $game{$attr} // ()
75 },
76}
77
78our %max_subs = (
79 maxhp => make_attr_sub 'maxhp',
80 maxpoints => make_attr_sub 'points',
81 maxconducts => make_attr_sub 'nconducts',
82);
83
84our %min_subs = (
85 minturns => make_attr_sub 'turns',
86 minrealtime => make_attr_sub 'realtime',
87);
88
89sub naodash_xlog { ## no critic (RequireArgUnpacking)
fd3d16b1
MG
90 my (%args, %exclude, %include);
91 %args = %{shift()} if ref $_[0] eq 'HASH'; ## no critic (Builtin)
92 %exclude = map { $_ => 1 } @{$args{exclude_versions} // []};
93 %include = map { $_ => 1 } @{$args{include_versions} // []};
01ba3ddc
MG
94 my ($xlog) = join '', @_;
95 my %number_subs = (%sum_subs, %max_subs, %min_subs);
96
97 my @checks;
98 my %numbers = map { $_ => [] } keys %number_subs;
99
100 for my $logline (split /\n/, $xlog) {
101 my %game = %{parse_xlogline $logline};
102 for (keys %game) {
103 delete $game{$_} if $game{$_} eq ''
104 }
fd3d16b1 105 next if $exclude{$game{version}} || %include && !$include{$game{version}};
01ba3ddc
MG
106 next if $game{flags} & 3; # flag 0x01 is wizard mode, 0x02 is explore mode
107 push @checks, $_->(%game) for @check_subs;
108 push @{$numbers{$_}}, $number_subs{$_}->(%game) for keys %number_subs;
109 }
110
111 $numbers{$_} = sum @{$numbers{$_}} for keys %sum_subs;
112 $numbers{$_} = max @{$numbers{$_}} for keys %max_subs;
113 $numbers{$_} = min @{$numbers{$_}} for keys %min_subs;
114 @checks = uniq map { lc } @checks;
115
116 {checks => [sort @checks], numbers => \%numbers}
117}
118
119my $ht = HTTP::Tiny->new(agent => "NetHack-NAOdash/$VERSION ");
120
49bfce9d 121sub _get_xlog_from_server {
01ba3ddc
MG
122 my ($name) = @_;
123 my $ret = $ht->get("http://alt.org/nethack/player-all-xlog.php?player=$name");
124 die 'Error while retrieving xlogfile from alt.org: ' . $ret->{status} . ' ' . $ret->{reason} . "\n" unless $ret->{success};
49bfce9d
MG
125 $ret->{content} =~ m{<pre>(.*)</pre>}i;
126}
127
128sub _get_xlog {
129 my ($name) = @_;
130 return _get_xlog_from_server $name if $ENV{NAODASH_CACHE} && lc $ENV{NAODASH_CACHE} eq 'none';
131 my $dir = $ENV{NAODASH_CACHE} || catdir tmpdir, 'naodash';
132 mkdir $dir or die "Cannot create cache directory: $!\n" unless -d $dir;
133 my $file = catfile $dir, $name;
3b893b63 134 write_file $file, _get_xlog_from_server $name if ! -f $file || time - (stat $file)[9] >= 86_400;
49bfce9d
MG
135 scalar read_file $file
136}
137
fd3d16b1
MG
138sub naodash_user { ## no critic (RequireArgUnpacking)
139 my $args = {};
140 $args = shift if ref $_[0] eq 'HASH';
49bfce9d
MG
141 my ($name) = @_;
142 my $xlog = _get_xlog $name;
01ba3ddc 143 die "No xlogfile found for user $name\n" unless defined $xlog;
fd3d16b1 144 naodash_xlog $args, $xlog;
01ba3ddc
MG
145}
146
1471;
148__END__
149
150=encoding utf-8
151
152=head1 NAME
153
154NetHack::NAOdash - Analyze NetHack xlogfiles and extract statistics
155
156=head1 SYNOPSIS
157
158 use NetHack::NAOdash;
159 my $stats = naodash_user 'mgv'; # Retrieve and analyze mgv's xlogfile from alt.org
fd3d16b1 160 my @checks = @{$stats->{checks}}; # List of 'achievements' obtained by mgv
01ba3ddc 161 my %checks = map { $_ => 1 } @checks;
fd3d16b1
MG
162 say 'mgv has ascended an orcish rogue' if $checks{combo_rog_orc_cha};
163 say 'mgv has ascended an atheist character' if $checks{conduct_atheist};
01ba3ddc
MG
164 my %numbers = %{$stats->{numbers}};
165 say "mgv has ascended $numbers{ascensions} out of $numbers{games} games";
166 say "mgv has spent $numbers{totalrealtime} seconds playing NetHack on NAO";
167
fd3d16b1
MG
168 $stats = naodash_user {include_versions => ['3.6.0']}, 'mgv';
169 say 'mgv has ascended an orcish rogue in 3.6.0' if $checks{combo_rog_orc_cha};
170 $stats = naodash_user {exclude_versions => ['3.6.0']}, 'mgv';
171 say 'mgv has ascended an atheist character pre-3.6.0' if $checks{conduct_atheist};
172
01ba3ddc
MG
173 use File::Slurp;
174 $stats = naodash_xlog read_file 'path/to/my/xlogfile';
175 %checks = map { $_ => 1 } @{$stats->{checks}};
fd3d16b1 176 say 'I have ascended a survivor' if $checks{uconduct_survivor};
01ba3ddc
MG
177
178=head1 DESCRIPTION
179
180NetHack::NAOdash analyzes a NetHack xlogfile and reports statistics.
181There are two types of statistics: B<checks>, which are flags
182(booleans) and B<numbers> which are integers.
183
184The B<checks> are tracked across all games. That is, a B<check> will
185be true in the statistics if it is true in at least one game. Except
186for B<checks> in the I<Achievements> category, only games that end in
187an ascension are considered for awarding a B<check>.
188
189The B<checks>, sorted by category, are:
190
191=over
192
193=item B<Achievements>
194
195These start with C<achieve_> and represent significant milestones in a
196game. They are usually relevant only for users who never ascended, as
197a game that ends in an ascension generally meets all of them.
198
199 achieve_sokoban achieve_luckstone achieve_medusa achieve_bell
200 achieve_gehennom achieve_candelabrum achieve_book achieve_invocation
201 achieve_amulet achieve_endgame achieve_astral achieve_ascended
202
203=item B<Starting Combos>
204
205These look like C<combo_role_race_alignment> and represent
206role/race/alignment combinations in ascended games. The starting
207alignment, not the alignment at the end of the game is considered. For
208example, C<cav_gno_neu> is true if the user ascended at least one
209gnomish caveman.
210
211=item B<Conducts>
212
213These start with C<conduct_> and represent the 12 officially tracked
214conducts.
215
216 conduct_foodless conduct_vegan conduct_vegetarian
217 conduct_atheist conduct_weaponless conduct_pacifist
218 conduct_illiterate conduct_genocideless conduct_polypileless
219 conduct_polyselfless conduct_wishless conduct_artiwishless
220
221=item B<Unofficial Conducts>
222
223These start with C<uconduct_> and represent conducts that are not
224officially tracked by the game.
225
226 uconduct_survivor uconduct_bones uconduct_minscore
227
228=back
229
230The numbers are:
231
232=over
233
234=item B<totalrealtime>
235
236The total time spent playing NetHack on NAO, in seconds.
237
238=item B<games>
239
240The number of games played.
241
242=item B<ascensions>
243
244The number of games played that ended in an ascension.
245
246=item B<maxhp>
247
248The highest maxHP at the end of an ascension.
249
250=item B<maxpoints>
251
252The highest score obtained at the end of an ascension.
253
254=item B<maxconducts>
255
256The maximum number of conducts at the end of an ascension.
257
258=item B<minturns>
259
260The minimum turns across ascended games.
261
262=item B<minrealtime>
263
264The minimum realtime across ascended games, in seconds.
265
266=back
267
268This module exports two functions:
269
270=over
271
fd3d16b1
MG
272=item B<naodash_xlog>([\%args], I<@lines>)
273
274=item B<naodash_xlog>([\%args], I<$xlog>)
275
276Takes an optional hashref followed by the contents of an xlogfile and
277returns the results of the analysis. The contents are joined together
278then split by the newline character, so they can be specified as a
279single string, as a list of lines, or as a combination thereof.
280
281The following keys are recognised in the optional hashref:
282
283=over
01ba3ddc 284
fd3d16b1 285=item include_versions
01ba3ddc 286
fd3d16b1
MG
287The associated value is an arrayref of NetHack versions that should be
288considered. Any game that was played on a version that is not in this
289arrayref will be ignored. If this key is not present or the value is
290an empty arrayref, all versions are considered.
291
292=item exclude_versions
293
294The associated value is an arrayref of NetHack versions that should
295not be considered. Any game that was played on a version that is in
296this arrayref will be ignored. If a version is both included and
297excluded at the same time, it will not be considered (in other words,
298exclude_versions overrides include_versions).
299
300=back
01ba3ddc
MG
301
302The return value is of the following form:
303
304 { checks => ['achieve_sokoban', 'achieve_luckstone', ...],
305 numbers => {totalrealtime => 12345, games => 2, ...} }
306
307In other words, C<< @{$result->{checks}} >> is an array of B<checks>
308that are true and C<< %{$result->{numbers}} >> is a hash of
309B<numbers>.
310
fd3d16b1 311=item B<naodash_user>([I<\%args>], I<$nao_username>)
01ba3ddc
MG
312
313Retrieves the xlogfile of a user from NAO and gives it to
314B<naodash_xlog>. Dies if no xlogfile is found or if the server cannot
315be contacted.
316
fd3d16b1
MG
317An optional hashref can be passed as a first argument. In this case it
318will be supplied as a first argument to B<naodash_xlog>, see that
319function's documentation for an explanation of useful keys.
320
49bfce9d
MG
321This method caches the downloaded xlogfiles for one day in the
322directory named by the NAODASH_CACHE environment variable.
323
324=back
325
326=head1 ENVIRONMENT
327
328=over
329
330=item NAODASH_CACHE
331
332Path to a directory that should be used to cache xlogfiles downloaded
333from NAO, or the special value 'none' (case-insensitive) to disable
334caching.
335
336By default a directory named 'naodash' in the default temporary
337directory (C<< File::Spec->tmpdir >>) is used.
338
01ba3ddc
MG
339=back
340
341=head1 SEE ALSO
342
343L<App::NAOdash>, L<App::Web::NAOdash>, L<http://alt.org/nethack/>
344
345=head1 AUTHOR
346
347Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
348
349=head1 COPYRIGHT AND LICENSE
350
351Copyright (C) 2015 by Marius Gavrilescu
352
353This library is free software; you can redistribute it and/or modify
354it under the same terms as Perl itself, either Perl version 5.20.2 or,
355at your option, any later version of Perl 5 you may have available.
356
357
358=cut
This page took 0.029968 seconds and 4 git commands to generate.