chiark / gitweb /
c5c2ab7e3b8d042de5660aaef9f4f96d2940865b
[ypp-sc-tools.web-live.git] / yarrg / web / routetrade
1 <%doc>
2
3  This is part of the YARRG website.  YARRG is a tool and website
4  for assisting players of Yohoho Puzzle Pirates.
5
6  Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
7  Copyright (C) 2009 Clare Boothby
8
9   YARRG's client code etc. is covered by the ordinary GNU GPL (v3 or later).
10   The YARRG website is covered by the GNU Affero GPL v3 or later, which
11    basically means that every installation of the website will let you
12    download the source.
13
14  This program is free software: you can redistribute it and/or modify
15  it under the terms of the GNU Affero General Public License as
16  published by the Free Software Foundation, either version 3 of the
17  License, or (at your option) any later version.
18
19  This program is distributed in the hope that it will be useful,
20  but WITHOUT ANY WARRANTY; without even the implied warranty of
21  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  GNU Affero General Public License for more details.
23
24  You should have received a copy of the GNU Affero General Public License
25  along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
27  Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
28  are used without permission.  This program is not endorsed or
29  sponsored by Three Rings.
30
31
32  This Mason component is the core trade planner for a specific route.
33
34
35 </%doc>
36 <%args>
37 $dbh
38 @islandids
39 @archipelagoes
40 $qa
41 </%args>
42 <%perl>
43
44 my @flow_conds;
45 my @query_params;
46
47 my $sd_condition= sub {
48         my ($bs, $ix) = @_;
49         my $islandid= $islandids[$ix];
50         if (defined $islandid) {
51                 return "${bs}.islandid = $islandid";
52         } else {
53                 push @query_params, $archipelagoes[$ix];
54                 return "${bs}_islands.archipelago = ?";
55         }
56 };
57
58 my %islandpair;
59 # $islandpair{$a,$b}= [ $start_island_ix, $end_island_ix ]
60
61 my $specific= !grep { !defined $_ } @islandids;
62 my $confusing= 0;
63
64 foreach my $src_i (0..$#islandids) {
65         my $src_isle= $islandids[$src_i];
66         my $src_cond= $sd_condition->('sell',$src_i);
67         my @dst_conds;
68         foreach my $dst_i ($src_i..$#islandids) {
69                 my $dst_isle= $islandids[$dst_i];
70                 my $dst_cond= $sd_condition->('buy',$dst_i);
71                 if ($dst_i==$src_i and !defined $src_isle) {
72                         # we always want arbitrage, but mentioning an arch
73                         # once shouldn't produce intra-arch trades
74                         $dst_cond=
75                                 "($dst_cond AND sell.islandid = buy.islandid)";
76                 }
77                 push @dst_conds, $dst_cond;
78
79                 if ($specific && !$confusing &&
80                     # With a circular route, do not carry goods round the loop
81                     !($src_i==0 && $dst_i==$#islandids &&
82                       $src_isle == $islandids[$dst_i])) {
83                         if ($islandpair{$src_isle,$dst_isle}) {
84                                 $confusing= 1;
85                         } else {
86                                 $islandpair{$src_isle,$dst_isle}=
87                                         [ $src_i, $dst_i ];
88                         }
89                 }
90         }
91         push @flow_conds, "$src_cond AND (
92                         ".join("
93                      OR ",@dst_conds)."
94                 )";
95 }
96
97 my $stmt= "             
98         SELECT  sell_islands.islandname                         org_name,
99                 sell_islands.islandid                           org_id,
100                 sell.price                                      org_price,
101                 buy_islands.islandname                          dst_name,
102                 buy_islands.islandid                            dst_id,
103                 buy.price                                       dst_price,
104 ".($qa->{ShowStalls} ? "
105                 sell.stallid                                    org_stallid,
106                 sell_stalls.stallname                           org_stallname,
107                 sell.qty                                        org_qty,
108                 buy.stallid                                     dst_stallid,
109                 buy_stalls.stallname                            dst_stallname,
110                 buy.qty                                         dst_qty,
111 " : "
112                 sum(sell.qty)                                   org_qty,
113                 sum(buy.qty)                                    dst_qty,
114 ")."
115                 commods.commodname                              commodname,
116                 commods.commodid                                commodid,
117                 commods.unitmass                                unitmass,
118                 commods.unitvolume                              unitvolume,
119                 buy.price - sell.price                          unitprofit
120         FROM commods
121         JOIN buy  on commods.commodid = buy.commodid
122         JOIN sell on commods.commodid = sell.commodid
123         JOIN islands as sell_islands on sell.islandid = sell_islands.islandid
124         JOIN islands as buy_islands  on buy.islandid  = buy_islands.islandid
125 ".($qa->{ShowStalls} ? "
126         JOIN stalls  as sell_stalls  on sell.stallid  = sell_stalls.stallid
127         JOIN stalls  as buy_stalls   on buy.stallid   = buy_stalls.stallid
128 " : "")."
129         WHERE   (
130                 ".join("
131            OR   ", @flow_conds)."
132         )
133           AND   buy.price > sell.price
134 ".($qa->{ShowStalls} ? "" : "
135         GROUP BY commods.commodid, org_id, org_price, dst_id, dst_price
136 ")."
137         ORDER BY org_name, dst_name, commodname, unitprofit DESC,
138                  org_price, dst_price DESC
139      ";
140
141 my $sth= $dbh->prepare($stmt);
142 $sth->execute(@query_params);
143 my @flows;
144
145 my @columns;
146 if ($qa->{ShowStalls}) {
147         push @columns,       qw(org_name org_stallname dst_name dst_stallname);
148 } else {
149         push @columns,       qw(org_name dst_name);
150 }
151 push @columns,               qw(commodname
152                                 org_price org_qty dst_price dst_qty
153                                 unitprofit PctProfit
154                                 MaxQty MaxCapital MaxProfit);
155
156 </%perl>
157
158 % if ($qa->{'debug'}) {
159 <pre>
160 <% $stmt |h %>
161 <% join(' | ',@query_params) |h %>
162 </pre>
163 % }
164
165 % {
166 <& dumptable:start, sth => $sth &>
167 %   my $flow;
168 %   while ($flow= $sth->fetchrow_hashref()) {
169 %       $flow->{Ix}= @flows;
170 %       $flow->{Var}= "f$flow->{Ix}";
171 %       push @flows, $flow;
172 <& dumptable:row, sth => $sth, row => $flow &>
173 %   }
174 <& dumptable:end &>
175 % }
176
177 % my $optimise= $specific && !$confusing && @islandids>1;
178 % if (!$optimise) {
179
180 <p>
181 % if (@islandids<=1) {
182 Route is trivial.
183 % }
184 % if (!$specific) {
185 Route contains archipelago(es), not just specific islands.
186 % }
187 % if ($confusing) {
188 Route is complex - it visits the same island several times
189 and isn't a simple loop.
190 % }
191 Therefore, optimal trade pattern not calculated.
192
193 % } else { # ========== OPTMISATION ==========
194 <%perl>
195
196 my $cplex= "
197 Maximize
198
199   totalprofit:
200                   ".(join " +
201                   ", map { "$_->{unit_profit} $_->{Var}" } @flows)."
202
203 Subject To
204 ";
205
206 my %avail_csts;
207 foreach my $flow (@flows) {
208         foreach my $od (qw(org dst)) {
209                 my $cstname= join '_',
210                         'avail',
211                         $flow->{'commodid'},
212                         $od,
213                         $flow->{"${od}_id"},
214                         $flow->{"${od}_price"};
215                 push @{ $avail_csts{$cstname}{Flows} }, $flow->{Var};
216                 $avail_csts{$cstname}{Qty}= $flow->{"${od}_qty"};
217         }
218 }
219 foreach my $cstname (sort keys %avail_csts) {
220         my $c= $avail_csts{$cstname};
221         $cplex .= "
222    ".   sprintf("%-30s","$cstname:")." ".
223         join("+", @{ $c->{Flows} }).
224         " <= ".$c->{Qty}."\n";
225 }
226
227 $cplex.= "
228 Bounds
229         ".(join "
230         ", map { "$_->{Var} >= 0" } @flows)."
231
232 End
233 ";
234
235 if ($qa->{'debug'}) {
236 </%perl>
237 <pre>
238 <% $cplex |h %>
239 </pre>
240 <%perl>
241 }
242
243 {
244         my $input= pipethrough_prep();
245         print $input $cplex or die $!;
246         my $output= pipethrough_run_along($input, undef, 'glpsol',
247                 qw(glpsol --cpxlp /dev/stdin -o /dev/stdout));
248         print "<pre>\n" if $qa->{'debug'};
249         my $found_section= 0;
250         while (<$output>) {
251                 print encode_entities($_) if $qa->{'debug'};
252                 if (m/^\s*No\.\s+Column name\s+St\s+Activity\s/) {
253                         die if $found_section>0;
254                         $found_section= 1;
255                         next;
256                 }
257                 next unless $found_section==1;
258                 next if m/^[- ]+$/;
259                 if (!/\S/) {
260                         $found_section= 2;
261                         next;
262                 }
263                 my ($ix, $qty) =
264                         m/^\s*\d+\s+f(\d+)\s+\S+\s+(\d+)\s/ or die "$_ ?";
265                 my $flow= $flows[$ix] or die;
266                 $flow->{OptQty}= $qty;
267                 $flow->{OptProfit}= $flow->{'unitprofit'} * $qty;
268                 $flow->{OptCapital}= $flow->{OptQty} * $flow->{'org_price'};
269         }
270         print "</pre>\n" if $qa->{'debug'};
271         pipethrough_run_finish($output, 'glpsol');
272         die unless $found_section;
273 };
274
275 print join ' ', map { $_->{Optimal} } @flows;
276
277 push @columns, qw(OptQty OptCapital OptProfit);
278
279 </%perl>
280
281 % } # ========== OPTIMISATION ==========
282
283 % {
284 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
285 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
286 <table>
287 <tr>
288 <th<% $cdspan %>>Collect
289 <th<% $cdspan %>>Deliver
290 <th>
291 <th colspan=2>Collect
292 <th colspan=2>Deliver
293 <th colspan=2>Profit
294 <th colspan=3>Max
295 %       if ($optimise) {
296 <th colspan=3>Suggested
297 %       }
298
299 <tr>
300 <th>Island <% $cdstall %>
301 <th>Island <% $cdstall %>
302 <th>Commodity
303 <th>Price
304 <th>Qty
305 <th>Price
306 <th>Qty
307 <th>Unit
308 <th>Margin
309 <th>Qty
310 <th>Capital
311 <th>Profit
312 %       if ($optimise) {
313 <th>Qty
314 <th>Capital
315 <th>Profit
316 %       }
317 % }
318
319 % foreach my $flow (@flows) {
320 <tr>
321 %       foreach my $col (@columns) {
322 %               $flow->{MaxQty}= $flow->{'org_qty'} < $flow->{'dst_qty'}
323 %                       ? $flow->{'org_qty'} : $flow->{'dst_qty'};
324 %               $flow->{MaxProfit}= $flow->{MaxQty} * $flow->{'unitprofit'};
325 %               $flow->{PctProfit}= sprintf "%3.1f%%",
326 %                       $flow->{'dst_price'} * 100.0 / $flow->{'org_price'}
327 %                       - 100.0;
328 %               $flow->{MaxCapital}= $flow->{MaxQty} * $flow->{'org_price'};
329 <td><% $flow->{$col} |h %>
330 %       }
331 % }
332 </table>
333
334 <%init>
335 use CommodsWeb;
336 use Commods;
337 </%init>