1 ###RUN_PERL: #!/usr/bin/perl
4 # 2words is generated from 2words.1.pl.
6 # The wordgame interface
8 # Copyright (C) 2016 - 2017, 2023 Balthasar SzczepaĆski
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU Affero General Public License as
12 # published by the Free Software Foundation, either version 3 of the
13 # License, or (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU Affero General Public License for more details.
20 # You should have received a copy of the GNU Affero General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # use Encode::Locale ('decode_argv');
26 use Encode ('encode', 'decode');
28 ###PERL_LIB: use lib /botm/lib/bsta
30 'read_data_file', 'write_data_file',
34 'html_entity_encode_dec',
38 'STATE', 'INTF_STATE',
39 'fail_method', 'fail_content_type',
40 'print_html_start', 'print_html_end',
41 'print_html_head_start', 'print_html_head_end',
42 'print_html_body_start', 'print_html_body_end',
49 ###PERL_PATH_SEPARATOR: PATH_SEPARATOR = /
51 ###PERL_CGI_PATH: CGI_PATH = /bsta/
52 ###PERL_CGI_2WORDS_PATH: CGI_2WORDS_PATH = /bsta/2words
54 ###PERL_DATA_PATH: DATA_PATH = /botm/data/bsta/
55 ###PERL_DATA_DEFAULT_PATH: DATA_DEFAULT_PATH = /botm/data/bsta/default
56 ###PERL_DATA_LIST_PATH: DATA_LIST_PATH = /botm/data/bsta/list
57 ###PERL_DATA_SETTINGS_PATH: DATA_SETTINGS_PATH = /botm/data/bsta/settings
58 ###PERL_DATA_STATE_PATH: DATA_STATE_PATH = /botm/data/bsta/state
59 ###PERL_DATA_STORY_PATH: DATA_STORY_PATH = /botm/data/bsta/story
61 ###PERL_WWW_PATH: WWW_PATH = /botm/www/1190/bsta/
63 ###PERL_WEBSITE_NAME: WEBSITE_NAME = Bicycles on the Moon
65 ###PERL_STORY_LENGTH: STORY_LENGTH = 16
66 ###PERL_PAGE_LENGTH: PAGE_LENGTH = 16
67 ###PERL_FIRSTPAGE_LENGTH: FIRSTPAGE_LENGTH = 4
69 binmode STDIN, ':encoding(UTF-8)';
70 binmode STDOUT, ':encoding(UTF-8)';
71 binmode STDERR, ':encoding(UTF-8)';
110 delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
111 ###PERL_SET_PATH: $ENV{'PATH'} = /usr/local/bin:/usr/bin:/bin;
113 if ($ENV{'REQUEST_METHOD'} =~ /^(HEAD|GET|POST)$/) {
117 exit fail_method($ENV{'REQUEST_METHOD'}, 'GET, POST, HEAD');
120 %http = read_header_env(\%ENV);
121 %cgi = url_query_decode($ENV{'QUERY_STRING'});
122 if ($method eq 'POST') {
123 if ($http{'content-type'} eq 'application/x-www-form-urlencoded') {
124 my %cgi_post=url_query_decode( <STDIN> );
125 foreach my $ind (keys %cgi_post) {
126 $cgi{$ind} = $cgi_post{$ind};
129 # multipart not supported
131 exit fail_content_type($http{'content-type'}, $method);
134 $IP = get_remote_addr();
135 if ($ENV{'PATH_INFO'} =~ /^\/(.+)$/) {
141 if ($cgi{'words'} ne '') {
142 $words=$cgi{'words'};
145 %settings = read_data_file(DATA_SETTINGS_PATH());
146 %state = read_data_file(DATA_STATE_PATH());
147 $ong_state = int($state{'state'});
148 $cmd_clear = $settings{'password'}.' clear';
149 $cmd_clear_all = $settings{'password'}.' clearall';
152 if (open ($story_file,"+<:encoding(UTF-8)",DATA_STORY_PATH())){
154 if (flock($story_file,2)) {
157 %story = read_data_file($story_file);
159 if ($story{'lastip'} =~ /^.+$/) {
166 $last_letter = lc($story{'letter'});
167 $story_id = int($story{'id'});
168 $intf_pass = int($story{'pass'});
169 $intf_state = int($story{'state'});
170 $intf_mode = $intf_state & INTF_STATE->{'mode'};
171 $intf_pause = $intf_state & INTF_STATE->{'||'};
173 if ($IP ne $last_IP) {
181 ($words eq $cmd_clear) ||
182 ($words eq $cmd_clear_all) ||
186 ($words eq $cmd_clear_all) ||
191 $story{'content'} = '';
192 $story{'lastip' } = '0.0.0.0';
193 $story{'letter' } = '';
195 $story{'state' } = INTF_STATE->{'X'};
197 if ($ong_state == STATE->{'inactive'}) {
206 write_data_file($story_file, '', '', \%story);
211 $message = "It's not your turn.";
213 # TODO: consider allowing non-ASCII letters in words.
214 # (not very important in English language)
215 elsif ($words =~ /^([!"\(\),\.:;\?][ \t]*)?([A-Za-z][A-Za-z'\-]*[A-Za-z']?)([!"\(\),\.:;\? \t][ \t]*)([A-Za-z][A-Za-z'\-]*[A-Za-z']?)([!"\(\),\.:;\?]?[ \t]*)$/) {
217 $first_letter = lc(substr($2, 0, 1));
218 $second_letter = lc(substr($4, 0, 1));
220 ($first_letter ne $last_letter) &&
223 $message = 'The first word must start with '.uc($last_letter).'.';
225 elsif ($first_letter eq $second_letter) {
226 $message = 'The second word can\'t also start with '.uc($first_letter).'.';
231 $story{'content'} = $story{'content'} . $words."\n";
233 $story{'lastip'} = $IP;
234 $story{'letter'} = $second_letter;
236 if ($cgi{'next'} ne '') {
238 if (split(/\r?\n/,$story{'content'}) >= (STORY_LENGTH-1)) {
239 # store finished game
240 $story_i_path = DATA_STORY_PATH.$story_id;
241 write_data_file($story_i_path, '', '', \%story);
243 $new_story{'id' } = $story_id + 1;
244 $new_story{'letter' } = '';
245 $new_story{'lastip' } = $IP;
246 $new_story{'content'} = '';
247 $new_story{'pass' } = 0;
248 $new_story{'state' } = INTF_STATE->{'X'};
249 # reset hidden interface
250 $intf_state = INTF_STATE->{'X'};
252 $intf_mode = INTF_STATE->{'X'};
254 if($ong_state == STATE->{'inactive'}) {
255 # ONG not activated yet; reset index
265 write_data_file($story_file, '', '', \%new_story);
268 $message = 'To early to finish this wordgame.';
269 write_data_file($story_file, '', '', \%story);
274 if ($intf_pass == 1) {
275 # hidden interface was already active; deactivate
278 if($ong_state == STATE->{'inactive'}) {
288 elsif(lc($2).' '.lc($4) eq $settings{'unlock'}) {
289 # correct password for the hidden interface!
290 if ($intf_pass != 0) {
291 $message = 'The password has already been used in this story.';
293 elsif ($ong_state != STATE->{'inactive'}) {
294 # ONG already active, nothing to do here
307 # prepare to ONG frame 0!
309 $frame_data_path = join_path(PATH_SEPARATOR(), DATA_PATH(), 0);
310 %frame_data = read_data_file($frame_data_path);
311 %default = read_data_file(DATA_DEFAULT_PATH());
313 $ext = (defined($frame_data{'ext'})) ?
317 $frame_file = sprintf($settings{'frame'}, 0, $ext);
318 $in_path = join_path(PATH_SEPARATOR(), DATA_PATH(), $frame_file);;
319 $out_path = join_path(PATH_SEPARATOR(), WWW_PATH(), $frame_file);
321 # set ONG time of frame 0
322 # NOTE: might get overwritten later if ONG not launched
323 $frame_data{'ongtime'} = $time;
324 $frame_data{'timer'} = 0;
325 write_data_file($frame_data_path, '', '', \%frame_data);
327 # update the GOTO list with frame 0
328 $goto_list{'title-0' } = $frame_data{'title'};
329 $goto_list{'ongtime-0'} = $frame_data{'ongtime'};
330 write_data_file(DATA_LIST_PATH(), '', '', \%goto_list);
332 if(copy ($in_path, $out_path)) {
333 # new state of hidden interface
335 $intf_state = INTF_STATE->{'X'};
336 $intf_mode = INTF_STATE->{'X'};
339 $story{'state'} = INTF_STATE->{'X'};
350 write_data_file($story_file, '', '', \%story);
355 $message = 'Please, two words, not more, not less (some punctuation is allowed).';
361 ($ong_state == STATE->{'inactive'})
363 $intf_state = int($cgi{'s'}) & INTF_STATE->{'mask'};
364 $intf_mode = $intf_state & INTF_STATE->{'mode'};
365 $intf_pause = $intf_state & INTF_STATE->{'||'};
366 $story{'state'} = $intf_state;
374 write_data_file($story_file, '', '', \%story);
376 @story_lines = split(/\r?\n/, $story{'content'});
377 if(@story_lines & 1) {
384 print "Content-type: text/html\n\n";
385 if($method eq 'HEAD') {
389 my $max_page = int(($story_id - FIRSTPAGE_LENGTH - 1) / PAGE_LENGTH) + 1;
390 my $newer_available = ($page > 0);
391 my $older_available = ($page < $max_page);
392 my $show_intf = ($intf_pass == 1) && ($ong_state == STATE->{'inactive'});
396 (($page-1) * PAGE_LENGTH ) + FIRSTPAGE_LENGTH
399 my $id_stop = $story_id-1 - (($page*PAGE_LENGTH) + FIRSTPAGE_LENGTH);
404 my $bsta_url = CGI_PATH;
405 my $twowords_url = CGI_2WORDS_PATH;
409 my $newest_url = merge_url(
410 {'path' => $twowords_url},
413 if ($newer_available) {
414 $newer_url = merge_url(
415 {'path' => $twowords_url},
419 if ($older_available) {
420 $older_url = merge_url(
421 {'path' => $twowords_url},
424 $oldest_url = merge_url(
425 {'path' => $twowords_url},
426 {'path' => $max_page}
429 my $button_4_url = merge_url(
430 {'path' => $twowords_url},
431 {'query' => {'s' => (INTF_STATE->{'>'} | $intf_pause)}}
433 my $button_3_url = merge_url(
434 {'path' => $twowords_url},
435 {'query' => {'s' => (INTF_STATE->{'<<'} | $intf_pause)}}
437 my $button_2_url = merge_url(
438 {'path' => $twowords_url},
439 {'query' => {'s' => (INTF_STATE->{'>>'} | $intf_pause)}}
441 my $button_1_url = merge_url(
442 {'path' => $twowords_url},
443 {'query' => {'s' => INTF_STATE->{'X'}}}
445 my $button_0_url = merge_url(
446 {'path' => $twowords_url},
447 {'query' => {'s' => ($intf_pause ? $intf_mode : ($intf_mode | INTF_STATE->{'||'}))}}
449 my $button_5_img = merge_url(
450 {'path' => CGI_PATH()},
451 {'path' => 'intf-20.gif'}
453 my $button_4_img = merge_url(
454 {'path' => CGI_PATH()},
455 {'path' => 'intf-10'.(($intf_mode == INTF_STATE->{'>'}) ? '_' : '').'.gif'}
457 my $button_3_img = merge_url(
458 {'path' => CGI_PATH()},
459 {'path' => 'intf-08'.(($intf_mode == INTF_STATE->{'<<'}) ? '_' : '').'.gif'}
461 my $button_2_img = merge_url(
462 {'path' => CGI_PATH()},
463 {'path' => 'intf-04'.(($intf_mode == INTF_STATE->{'>>'}) ? '_' : '').'.gif'}
465 my $button_1_img = merge_url(
466 {'path' => CGI_PATH()},
467 {'path' => 'intf-02.gif'}
469 my $button_0_img = merge_url(
470 {'path' => CGI_PATH()},
471 {'path' => 'intf-01'.($intf_pause ? '_' : '').'.gif'}
473 my $intf_img_id = '';
474 if ($intf_state == INTF_STATE->{'>'}) {
477 elsif ($intf_mode == INTF_STATE->{'<<'}) {
480 elsif ($intf_mode == INTF_STATE->{'>>'}) {
483 my $intf_img = merge_url(
484 {'path' => CGI_PATH()},
485 {'path' => 'intf-00'.$intf_img_id.'.gif'}
488 my $_bsta_url = html_entity_encode_dec($bsta_url , 1);
489 my $_twowords_url = html_entity_encode_dec($twowords_url , 1);
490 my $_newest_url = html_entity_encode_dec($newest_url , 1);
491 my $_newer_url = html_entity_encode_dec($newer_url , 1);
492 my $_older_url = html_entity_encode_dec($older_url , 1);
493 my $_oldest_url = html_entity_encode_dec($oldest_url , 1);
494 my $_button_4_url = html_entity_encode_dec($button_4_url , 1);
495 my $_button_3_url = html_entity_encode_dec($button_3_url , 1);
496 my $_button_2_url = html_entity_encode_dec($button_2_url , 1);
497 my $_button_1_url = html_entity_encode_dec($button_1_url , 1);
498 my $_button_0_url = html_entity_encode_dec($button_0_url , 1);
499 my $_button_5_img = html_entity_encode_dec($button_5_img , 1);
500 my $_button_4_img = html_entity_encode_dec($button_4_img , 1);
501 my $_button_3_img = html_entity_encode_dec($button_3_img , 1);
502 my $_button_2_img = html_entity_encode_dec($button_2_img , 1);
503 my $_button_1_img = html_entity_encode_dec($button_1_img , 1);
504 my $_button_0_img = html_entity_encode_dec($button_0_img , 1);
505 my $_intf_img = html_entity_encode_dec($intf_img , 1);
506 my $_message = html_entity_encode_dec($message , 1);
507 my $_website_name = html_entity_encode_dec(WEBSITE_NAME(), 1);
509 print_html_start(\*STDOUT);
510 print_html_head_start(\*STDOUT);
513 print ' <title>Two words • '.$_website_name.'</title>'."\n";
514 print ' <link rel="start" href="'.$_oldest_url.'">'."\n";
515 if ($older_available) {
516 print ' <link rel="prev" href="'.$_older_url.'">'."\n";
518 if ($newer_available) {
519 print ' <link rel="next" href="'.$_newer_url.'">'."\n";
522 print_html_head_end(\*STDOUT);
523 print_html_body_start(\*STDOUT);
525 print ' <div id="inst" class="ins">'."\n";
527 print ' <div id="title">'."\n";
528 print ' <h1 id="titletext">Two words</h1>'."\n";
529 print ' </div>'."\n";
532 print ' <div id="storypuzzle">'."\n";
533 for (my $i = 0; $i < @story_lines; ++$i) {
534 print ' <span class="'.($turn ? 'ni':'br').'">'.html_entity_encode_dec($story_lines[$i], 1).'</span>'."\n";
537 print ' </div>'."\n";
539 print ' <div id="command">'."\n";
540 if ($message ne '') {
541 print ' <span class="br">'.$_message.'</span>'."\n";
545 print ' <form method="post" action="'.$_twowords_url.'">'."\n";
546 if ($message eq '') {
547 if ($story{"content"} eq '') {
548 print ' Two words, please:<br>'."\n";
551 print ' Please continue, two words:<br>'."\n";
554 print ' <input class="intx" type="text" name="words">'."\n";
555 print ' <input class="inbt" type="submit" value="enter">'."\n";
556 if(@story_lines >= (STORY_LENGTH-1)) {
557 print ' <input class="inbt" type="submit" name="next" value="enter and then start a new one">'."\n";
559 print ' </form>'."\n";
562 if ($message eq '') {
563 print ' Wait for it.'."\n";
566 print ' </div>'."\n";
568 elsif ($message ne '') {
569 print ' <div id="command">'."\n";
570 print ' <span class="br">'.$_message.'</span>'."\n";
571 print ' </div>'."\n";
573 print ' </div>'."\n";
576 print ' <div id="framespace">'."\n";
577 print ' <table id="intftable" cellspacing="0" cellpadding="0">'."\n";
578 print ' <tr class="intf">'."\n";
579 print ' <td colspan="6" class="intf"><img src="'.$_intf_img.'" alt="" class="intf"></td>'."\n";
582 print ' <tr class="intf">'."\n";
583 print ' <td class="intf"><img src="'.$_button_5_img.'" alt="o" class="intf"></td>'."\n";
584 print ' <td class="intf"><a href="'.$_button_4_url.'"><img src="'.$_button_4_img.'" class="intf" alt=">"></a></td>'."\n";
585 print ' <td class="intf"><a href="'.$_button_3_url.'"><img src="'.$_button_3_img.'" class="intf" alt="<<"></a></td>'."\n";
586 print ' <td class="intf"><a href="'.$_button_2_url.'"><img src="'.$_button_2_img.'" class="intf" alt=">>"></a></td>'."\n";
587 print ' <td class="intf"><a href="'.$_button_1_url.'"><img src="'.$_button_1_img.'" class="intf" alt="^"></a></td>'."\n";
588 print ' <td class="intf"><a href="'.$_button_0_url.'"><img src="'.$_button_0_img.'" class="intf" alt="||"></a></td>'."\n";
590 print ' </table>'."\n";
591 print ' </div>'."\n";
594 print ' <div id="insb" class="ins">'."\n";
596 print ' <div id="undertext">'."\n";
597 for (my $i = $id_start; $i > $id_stop; --$i) {
598 $story_i_path = DATA_STORY_PATH.$i;
599 %new_story = read_data_file($story_i_path);
600 print ' <p class="'.(($i&1)?'br':'ni').'" id="s'.$i.'">'.html_entity_encode_dec($new_story{'content'}).'</p>'."\n";
602 print ' </div>'."\n";
604 print ' <div id="underlinks">'."\n";
605 print ' <a href="'.$_bsta_url.'">BSTA</a> |'."\n";
606 print ' <a href="'.$_twowords_url.'">Once again</a>';
607 if ($older_available) {
609 print ' <a href="'.$_older_url.'">Before</a>';
611 if ($newer_available) {
613 print ' <a href="'.$newer_url.'">Unbefore</a>';
615 if ($older_available) {
617 print '<a href="'.$_oldest_url.'">Initially</a>';
621 print ' (Entering words here is irreversible. Your actions might be remembered forever. So please be reasonable.)';
624 print ' </div>'."\n";
626 print ' </div>'."\n";
628 print_html_body_end(\*STDOUT);
629 print_html_end(\*STDOUT);