OSDN Git Service

rename package
[jindolf/JinParser.git] / src / main / java / jp / osdn / jindolf / parser / SysEventParser.java
1 /*
2  * System event parser
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.osdn.jindolf.parser;
9
10 import java.util.regex.Pattern;
11 import jp.sourceforge.jindolf.corelib.EventFamily;
12 import jp.sourceforge.jindolf.corelib.GameRole;
13 import jp.sourceforge.jindolf.corelib.SysEventType;
14 import jp.sourceforge.jindolf.corelib.Team;
15
16 /**
17  * 人狼BBSシステムが出力する各種イベント表記のパースを行うパーサ。
18  * パース進行に従い{@link SysEventHandler}の各種メソッドが呼び出される。
19  */
20 @SuppressWarnings({
21     "checkstyle:declarationdistance",
22     "checkstyle:declarationorder",
23     "PMD.FieldDeclarationsShouldBeAtStartOfClass",
24     "PMD.PrematureDeclaration"
25 })
26 public class SysEventParser extends AbstractParser{
27
28     private static final String AVATAR_REGEX =
29             "[^<、" + SPCHAR + "]+\u0020[^<、。" + SPCHAR + "]+";
30
31     private static final Pattern C_DIV_PATTERN =
32             compile(SP_I+ "</div>" +SP_I);
33     private static final Pattern AVATAR_PATTERN =
34             compile(AVATAR_REGEX);
35
36
37     private SysEventHandler sysEventHandler;
38
39     private int pushedRegionStart = -1;
40     private int pushedRegionEnd   = -1;
41
42     private final SeqRange rangepool_1 = new SeqRange();
43     private final SeqRange rangepool_2 = new SeqRange();
44     private final SeqRange rangepool_3 = new SeqRange();
45
46     /**
47      * コンストラクタ。
48      * @param parent 親パーサ
49      */
50     public SysEventParser(ChainedParser parent){
51         super(parent);
52         return;
53     }
54
55     /**
56      * {@link SysEventHandler}ハンドラを登録する。
57      * @param sysEventHandler ハンドラ
58      */
59     public void setSysEventHandler(SysEventHandler sysEventHandler){
60         this.sysEventHandler = sysEventHandler;
61         return;
62     }
63
64     /**
65      * Announceメッセージをパースする。
66      * @throws HtmlParseException パースエラー
67      */
68     public void parseAnnounce() throws HtmlParseException{
69         setContextErrorMessage("Unknown Announce message");
70
71         this.sysEventHandler.startSysEvent(EventFamily.ANNOUNCE);
72
73         int regionStart = regionStart();
74         int regionEnd   = regionEnd();
75
76         boolean result =
77                    probeSimpleAnnounce()
78                 || probeOpenRole()
79                 || probeSurvivor()
80                 || probeMurdered()
81                 || probeOnStage()
82                 || probeSuddenDeath()
83                 || probeCounting()
84                 || probePlayerList()
85                 || probeExecution()
86                 || probeVanish()
87                 || probeCheckout()
88                 ;
89         if( ! result ){
90             throw buildParseException();
91         }
92
93         getMatcher().region(regionStart, regionEnd);
94         parseContent();
95
96         lookingAtAffirm(C_DIV_PATTERN);
97         shrinkRegion();
98
99         this.sysEventHandler.endSysEvent();
100
101         return;
102     }
103
104     private static final Pattern STARTENTRY_PATTERN =
105              compile(
106              "昼間は人間のふりをして、夜に正体を現すという人狼。<br />"
107             +"その人狼が、"
108             +"この村に紛れ込んでいるという噂が広がった。<br /><br />"
109             +"村人達は半信半疑ながらも、"
110             +"村はずれの宿に集められることになった。"
111             +"<br />");
112     private static final Pattern STARTMIRROR_PATTERN =
113              compile(
114              "さあ、自らの姿を鏡に映してみよう。<br />"
115             +"そこに映るのはただの村人か、"
116             +"それとも血に飢えた人狼か。<br /><br />"
117             +"例え人狼でも、多人数で立ち向かえば怖くはない。<br />"
118             +"問題は、だれが人狼なのかという事だ。<br />"
119             +"占い師の能力を持つ人間ならば、それを見破れるだろう。"
120             +"(?:<br />)?");
121     private static final Pattern STARTASSAULT_PATTERN =
122              compile(
123              "ついに犠牲者が出た。人狼はこの村人達のなかにいる。<br />"
124             +"しかし、それを見分ける手段はない。<br /><br />"
125             +"村人達は、疑わしい者を排除するため、"
126             +"投票を行う事にした。<br />"
127             +"無実の犠牲者が出るのもやむをえない。"
128             +"村が全滅するよりは……。<br /><br />"
129             +"最後まで残るのは村人か、それとも人狼か。"
130             +"(?:<br />)?");
131     private static final Pattern NOMURDER_PATTERN =
132              compile(
133              "今日は犠牲者がいないようだ。人狼は襲撃に失敗したのだろうか。");
134     private static final Pattern WINVILLAGE_PATTERN =
135              compile(
136              "全ての人狼を退治した……。人狼に怯える日々は去ったのだ!"
137             +"(?:<br />)?");
138     private static final Pattern WINWOLF_PATTERN =
139              compile(
140              "もう人狼に抵抗できるほど村人は残っていない……。<br />"
141             +"人狼は残った村人を全て食らい、"
142             +"別の獲物を求めてこの村を去っていった。"
143             +"(?:<br />)?");
144     private static final Pattern WINHAMSTER_PATTERN =
145              compile(
146               "全ては終わったかのように見えた。<br />"
147              +"だが、奴が生き残っていた……。");
148     private static final Pattern PANIC_PATTERN =
149              compile("……。");
150     private static final Pattern SHORTMEMBER_PATTERN =
151              compile(
152              "まだ村人達は揃っていないようだ。"
153             +"(?:<br />)?");
154
155     private static final Object[][] SIMPLE_REGEX_TO_TYPE = {
156         { STARTENTRY_PATTERN,   SysEventType.STARTENTRY   },
157         { STARTMIRROR_PATTERN,  SysEventType.STARTMIRROR  },
158         { STARTASSAULT_PATTERN, SysEventType.STARTASSAULT },
159         { NOMURDER_PATTERN,     SysEventType.NOMURDER     },
160         { WINVILLAGE_PATTERN,   SysEventType.WINVILLAGE   },
161         { WINWOLF_PATTERN,      SysEventType.WINWOLF      },
162         { WINHAMSTER_PATTERN,   SysEventType.WINHAMSTER   },
163         { PANIC_PATTERN,        SysEventType.PANIC        },
164         { SHORTMEMBER_PATTERN,  SysEventType.SHORTMEMBER  },
165     };
166
167     /**
168      * 文字列が固定されたシンプルなAnnounceメッセージのパースを試みる。
169      * @return マッチしたらtrue
170      * @throws HtmlParseException パースエラー
171      */
172     private boolean probeSimpleAnnounce() throws HtmlParseException{
173         pushRegion();
174
175         sweepSpace();
176
177         SysEventType matchedType = null;
178
179         for(Object[] pair : SIMPLE_REGEX_TO_TYPE){
180             Pattern pattern = (Pattern) pair[0];
181
182             if(lookingAtProbe(pattern)){
183                 shrinkRegion();
184                 matchedType = (SysEventType) pair[1];
185                 break;
186             }
187         }
188
189         if(matchedType == null){
190             popRegion();
191             return false;
192         }
193
194         this.sysEventHandler.sysEventType(matchedType);
195
196         sweepSpace();
197
198         return true;
199     }
200
201     private static final Pattern OPENROLE_HEAD_PATTERN =
202             compile("どうやらこの中には、");
203     private static final Pattern OPENROLE_NUM_PATTERN =
204             compile("が([0-9]+)名(?:、)?");
205     private static final Pattern OPENROLE_TAIL_PATTERN =
206             compile("いるようだ。");
207
208     /**
209      * OPENROLEメッセージのパースを試みる。
210      * @return マッチしたらtrue
211      * @throws HtmlParseException パースエラー
212      */
213     private boolean probeOpenRole() throws HtmlParseException{
214         pushRegion();
215
216         sweepSpace();
217
218         if( ! lookingAtProbe(OPENROLE_HEAD_PATTERN) ){
219             popRegion();
220             return false;
221         }
222         shrinkRegion();
223
224         this.sysEventHandler.sysEventType(SysEventType.OPENROLE);
225
226         for(;;){
227             GameRole role = lookingAtRole();
228             if(role == null){
229                 if( lookingAtProbe(OPENROLE_TAIL_PATTERN) ){
230                     shrinkRegion();
231                     break;
232                 }
233                 popRegion();
234                 return false;
235             }
236             shrinkRegion();
237
238             if( ! lookingAtProbe(OPENROLE_NUM_PATTERN) ){
239                 popRegion();
240                 return false;
241             }
242             int num = parseGroupedInt(1);
243             shrinkRegion();
244
245             this.sysEventHandler.sysEventOpenRole(role, num);
246         }
247
248         sweepSpace();
249
250         return true;
251     }
252
253     private static final Pattern SURVIVOR_HEAD_PATTERN =
254             compile("現在の生存者は、");
255     private static final Pattern SURVIVOR_PATTERN =
256             Pattern.compile(
257             "(" + AVATAR_REGEX + ")"
258             +"(?:"
259                 +"(?:"
260                     +"、"
261                 +")|(?:"
262                     +"\u0020の\u0020([0-9]+)\u0020名。"
263                 +")"
264             +")");
265
266     /**
267      * SURVIVORメッセージのパースを試みる。
268      * @return マッチしたらtrue
269      * @throws HtmlParseException パースエラー
270      */
271     private boolean probeSurvivor() throws HtmlParseException{
272         SeqRange avatarRange = this.rangepool_1;
273
274         pushRegion();
275
276         sweepSpace();
277
278         if( ! lookingAtProbe(SURVIVOR_HEAD_PATTERN) ){
279             popRegion();
280             return false;
281         }
282         shrinkRegion();
283
284         this.sysEventHandler.sysEventType(SysEventType.SURVIVOR);
285
286         int avatarNum = 0;
287         for(;;){
288             if( ! lookingAtProbe(SURVIVOR_PATTERN) ){
289                 popRegion();
290                 return false;
291             }
292             avatarRange.setLastMatchedGroupRange(getMatcher(), 1);
293             this.sysEventHandler
294                 .sysEventSurvivor(getContent(), avatarRange);
295             avatarNum++;
296             if(isGroupMatched(2)){
297                 int num = parseGroupedInt(2);
298                 shrinkRegion();
299                 if(num != avatarNum){
300                     throw new HtmlParseException(regionStart());
301                 }
302                 break;
303             }
304             shrinkRegion();
305         }
306
307         sweepSpace();
308
309         return true;
310     }
311
312     private static final Pattern MURDERED_HEAD_PATTERN =
313             compile("次の日の朝、");
314     private static final Pattern MURDERED_SW_PATTERN =
315             compile(
316                 "("
317                     +"\u0020と\u0020"
318                 +")|("
319                     +"\u0020が無残な姿で発見された。"
320                     +"(?:<br />)?"  // E国対策
321                 +")"
322             );
323
324     /**
325      * MURDEREDメッセージのパースを試みる。
326      * @return マッチしたらtrue
327      * @throws HtmlParseException パースエラー
328      */
329     private boolean probeMurdered() throws HtmlParseException{
330         SeqRange avatarRange  = this.rangepool_1;
331         SeqRange avatarRange2 = this.rangepool_2;
332         avatarRange .setInvalid();
333         avatarRange2.setInvalid();
334
335         pushRegion();
336
337         sweepSpace();
338
339         if( ! lookingAtProbe(MURDERED_HEAD_PATTERN)){
340             popRegion();
341             return false;
342         }
343         shrinkRegion();
344
345         this.sysEventHandler.sysEventType(SysEventType.MURDERED);
346
347         for(;;){
348             if( ! lookingAtProbe(AVATAR_PATTERN)){
349                 popRegion();
350                 return false;
351             }
352             if( ! avatarRange.isValid() ){
353                 avatarRange.setLastMatchedRange(getMatcher());
354             }else if( ! avatarRange2.isValid() ){
355                 avatarRange2.setLastMatchedRange(getMatcher());
356             }else{
357                 assert false;
358                 throw buildParseException();
359             }
360             shrinkRegion();
361
362             if( ! lookingAtProbe(MURDERED_SW_PATTERN)){
363                 popRegion();
364                 return false;
365             }
366             if(isGroupMatched(1)){
367                 shrinkRegion();
368                 continue;
369             }else if(isGroupMatched(2)){
370                 shrinkRegion();
371                 break;
372             }else{
373                 assert false;
374                 throw buildParseException();
375             }
376         }
377
378         this.sysEventHandler
379             .sysEventMurdered(getContent(), avatarRange);
380         if(avatarRange2.isValid()){
381             this.sysEventHandler
382                 .sysEventMurdered(getContent(), avatarRange2);
383         }
384
385         sweepSpace();
386
387         return true;
388     }
389
390     private static final Pattern ONSTAGE_NO_PATTERN =
391             compile("([0-9]+)人目、");
392     private static final Pattern ONSTAGE_DOT_PATTERN =
393             compile(
394              "("
395             +"(?:" + AVATAR_REGEX + ")"
396             +"|)"    // F1556プロローグ対策
397             +"。");
398
399     /**
400      * ONSTAGEメッセージのパースを試みる。
401      * @return マッチしたらtrue
402      * @throws HtmlParseException パースエラー
403      */
404     private boolean probeOnStage() throws HtmlParseException{
405         SeqRange avatarRange = this.rangepool_1;
406
407         pushRegion();
408
409         sweepSpace();
410
411         if( ! lookingAtProbe(ONSTAGE_NO_PATTERN) ){
412             popRegion();
413             return false;
414         }
415         int entryNo = parseGroupedInt(1);
416         shrinkRegion();
417
418         this.sysEventHandler.sysEventType(SysEventType.ONSTAGE);
419
420         if( ! lookingAtProbe(ONSTAGE_DOT_PATTERN) ){
421             popRegion();
422             return false;
423         }
424         avatarRange.setLastMatchedGroupRange(getMatcher(), 1);
425         shrinkRegion();
426
427         this.sysEventHandler
428             .sysEventOnStage(getContent(), entryNo, avatarRange);
429
430         sweepSpace();
431
432         return true;
433     }
434
435     private static final Pattern SUDDENDEATH_PATTERN =
436             compile(
437                  "("
438                 +"(?:" + AVATAR_REGEX + ")"
439                 +"|)"                            // F681 2d 対策
440                 +"\u0020?は、突然死した。"
441             );
442
443     /**
444      * SUDDENDEATHメッセージのパースを試みる。
445      * @return マッチしたらtrue
446      * @throws HtmlParseException パースエラー
447      */
448     private boolean probeSuddenDeath() throws HtmlParseException{
449         SeqRange avatarRange = this.rangepool_1;
450
451         pushRegion();
452
453         sweepSpace();
454
455         if( ! lookingAtProbe(SUDDENDEATH_PATTERN)){
456             popRegion();
457             return false;
458         }
459         avatarRange.setLastMatchedGroupRange(getMatcher(), 1);
460         shrinkRegion();
461
462         this.sysEventHandler.sysEventType(SysEventType.SUDDENDEATH);
463         this.sysEventHandler
464             .sysEventSuddenDeath(getContent(), avatarRange);
465
466         sweepSpace();
467
468         return true;
469     }
470
471     private static final Pattern COUNTING_PATTERN =
472             compile(
473             "(?:"
474                 +"<br />"
475                 +"(" + AVATAR_REGEX + ")"
476                 +"\u0020は村人達の手により処刑された。"
477             +")|(?:"
478                 +"(" + AVATAR_REGEX + ")"
479                 +"\u0020は\u0020"
480                 +"(" + AVATAR_REGEX + ")"
481                 +"\u0020に投票した。"
482                 +"(?:<br />)?"
483             +")"
484             );
485
486     /**
487      * COUNTINGメッセージのパースを試みる。
488      * @return マッチしたらtrue
489      * @throws HtmlParseException パースエラー
490      */
491     private boolean probeCounting() throws HtmlParseException{
492         SeqRange voteByRange = this.rangepool_1;
493         SeqRange voteToRange = this.rangepool_2;
494
495         pushRegion();
496
497         sweepSpace();
498
499         boolean hasVote = false;
500         for(;;){
501             if( ! lookingAtProbe(COUNTING_PATTERN) ){
502                 break; // 処刑なし
503             }
504             if(isGroupMatched(1)){
505                 voteByRange.setInvalid();
506                 voteToRange.setLastMatchedGroupRange(getMatcher(), 1);
507                 shrinkRegion();
508                 this.sysEventHandler
509                     .sysEventCounting(getContent(),
510                                       voteByRange,
511                                       voteToRange );
512                 break;
513             }else if(isGroupMatched(2)){
514                 if( ! hasVote ){
515                     hasVote = true;
516                     this.sysEventHandler.sysEventType(SysEventType.COUNTING);
517                 }
518                 voteByRange.setLastMatchedGroupRange(getMatcher(), 2);
519                 voteToRange.setLastMatchedGroupRange(getMatcher(), 3);
520                 shrinkRegion();
521                 this.sysEventHandler
522                     .sysEventCounting(getContent(),
523                                       voteByRange,
524                                       voteToRange );
525             }else{
526                 assert false;
527                 throw buildParseException();
528             }
529         }
530
531         if( ! hasVote ){
532             popRegion();
533             return false;
534         }
535
536         sweepSpace();
537
538         return true;
539     }
540
541     private static final Pattern COUNTING2_PATTERN =
542             compile(
543                  "(" + AVATAR_REGEX + ")"
544                 +"\u0020は\u0020"
545                 +"(" + AVATAR_REGEX + ")"
546                 +"\u0020に投票した。"
547                 +"(?:<br />)?"
548             );
549
550     /**
551      * COUNTING2メッセージのパースを試みる。
552      * @return マッチしたらtrue
553      * @throws HtmlParseException パースエラー
554      */
555     private boolean probeCounting2() throws HtmlParseException{
556         SeqRange voteByRange = this.rangepool_1;
557         SeqRange voteToRange = this.rangepool_2;
558
559         pushRegion();
560
561         sweepSpace();
562
563         boolean hasVote = false;
564         for(;;){
565             if( ! lookingAtProbe(COUNTING2_PATTERN) ){
566                 break;
567             }
568             if( ! hasVote ){
569                 hasVote = true;
570                 this.sysEventHandler.sysEventType(SysEventType.COUNTING2);
571             }
572             voteByRange.setLastMatchedGroupRange(getMatcher(), 1);
573             voteToRange.setLastMatchedGroupRange(getMatcher(), 2);
574             shrinkRegion();
575             this.sysEventHandler
576                 .sysEventCounting2(getContent(),
577                                    voteByRange,
578                                    voteToRange );
579         }
580
581         if( ! hasVote ){
582             popRegion();
583             return false;
584         }
585
586         sweepSpace();
587
588         return true;
589     }
590
591     private static final Pattern PLAYERID_PATTERN =
592             compile(
593                 "\u0020\uff08" // 全角開き括弧
594                 +"(?:<a\u0020href=\"([^\"]*)\">)?"
595                 +"([^<]*)"
596                 +"(?:</a>)?"
597                 +"\uff09、"     // 全角閉じ括弧
598             );
599     private static final Pattern LIVEORDIE_PATTERN =
600             compile(
601                 "(生存。)|(死亡。)"
602             );
603     private static final Pattern PLAYER_DELIM_PATTERN =
604             compile(
605                  "だった。"
606                 +"(?:<br />)?"
607             );
608
609     /**
610      * PLAYERLISTメッセージのパースを試みる。
611      * @return マッチしたらtrue
612      * @throws HtmlParseException パースエラー
613      */
614     private boolean probePlayerList() throws HtmlParseException{
615         SeqRange avatarRange  = this.rangepool_1;
616         SeqRange anchorRange  = this.rangepool_2;
617         SeqRange accountRange = this.rangepool_3;
618
619         pushRegion();
620
621         sweepSpace();
622
623         boolean hasPlayerList = false;
624
625         for(;;){
626             if( ! lookingAtProbe(AVATAR_PATTERN)){
627                 break;
628             }
629             avatarRange.setLastMatchedRange(getMatcher());
630             shrinkRegion();
631
632             if( ! lookingAtProbe(PLAYERID_PATTERN)){
633                 popRegion();
634                 return false;
635             }
636             if(isGroupMatched(1)){
637                 anchorRange.setLastMatchedGroupRange(getMatcher(), 1);
638             }else{
639                 anchorRange.setInvalid();
640             }
641             accountRange.setLastMatchedGroupRange(getMatcher(), 2);
642             shrinkRegion();
643
644             boolean isLiving = false;
645             if( ! lookingAtProbe(LIVEORDIE_PATTERN)){
646                 popRegion();
647                 return false;
648             }
649             if(isGroupMatched(1)){
650                 isLiving = true;
651             }else if(isGroupMatched(2)){
652                 isLiving = false;
653             }
654             shrinkRegion();
655
656             GameRole role = lookingAtRole();
657             if(role == null){
658                 popRegion();
659                 return false;
660             }
661             shrinkRegion();
662
663             if( ! lookingAtProbe(PLAYER_DELIM_PATTERN)){
664                 popRegion();
665                 return false;
666             }
667             shrinkRegion();
668
669             if( ! hasPlayerList ){
670                 hasPlayerList = true;
671                 this.sysEventHandler.sysEventType(SysEventType.PLAYERLIST);
672             }
673
674             this.sysEventHandler
675                 .sysEventPlayerList(getContent(),
676                                     avatarRange,
677                                     anchorRange,
678                                     accountRange,
679                                     isLiving,
680                                     role );
681         }
682
683         if( ! hasPlayerList ){
684             popRegion();
685             return false;
686         }
687
688         sweepSpace();
689
690         return true;
691     }
692
693     private static final Pattern EXECUTION_PATTERN =
694             compile(
695                 "(?:"
696                 + "(" + AVATAR_REGEX + ")、([0-9]+)票。(?:<br />)?"
697                 +")|(?:"
698                 +"<br />(" + AVATAR_REGEX + ")\u0020は"
699                 +"村人達の手により処刑された。"
700                 +")"
701             );
702
703     /**
704      * EXECUTIONメッセージのパースを試みる。
705      * @return マッチしたらtrue
706      * @throws HtmlParseException パースエラー
707      */
708     private boolean probeExecution() throws HtmlParseException{
709         SeqRange avatarRange  = this.rangepool_1;
710
711         pushRegion();
712
713         sweepSpace();
714
715         boolean hasExecution = false;
716
717         for(;;){
718             if( ! lookingAtProbe(EXECUTION_PATTERN)){
719                 break;
720             }
721
722             if( ! hasExecution ){
723                 hasExecution = true;
724                 this.sysEventHandler.sysEventType(SysEventType.EXECUTION);
725             }
726
727             if(isGroupMatched(1)){
728                 avatarRange.setLastMatchedGroupRange(getMatcher(), 1);
729                 int votes = parseGroupedInt(2);
730                 shrinkRegion();
731                 this.sysEventHandler
732                     .sysEventExecution(getContent(),
733                                        avatarRange,
734                                        votes );
735             }else if(isGroupMatched(3)){
736                 avatarRange.setLastMatchedGroupRange(getMatcher(), 3);
737                 shrinkRegion();
738                 this.sysEventHandler
739                     .sysEventExecution(getContent(),
740                                        avatarRange,
741                                        -1 );
742             }
743         }
744
745         if( ! hasExecution ){
746             popRegion();
747             return false;
748         }
749
750         sweepSpace();
751
752         return true;
753     }
754
755     private static final Pattern VANISH_PATTERN =
756             compile(
757                  "(?:<br />)*"
758                 +"(" + AVATAR_REGEX + ")"
759                 +"\u0020は、失踪した。"
760                 +"(?:<br />)*"
761             );
762
763     /**
764      * VANISHメッセージのパースを試みる。
765      * @return マッチしたらtrue
766      * @throws HtmlParseException パースエラー
767      */
768     private boolean probeVanish() throws HtmlParseException{
769         SeqRange avatarRange  = this.rangepool_1;
770
771         pushRegion();
772
773         sweepSpace();
774
775         boolean hasVanish = false;
776
777         for(;;){
778             if( ! lookingAtProbe(VANISH_PATTERN)){
779                 break;
780             }
781
782             if( ! hasVanish ){
783                 hasVanish = true;
784                 this.sysEventHandler.sysEventType(SysEventType.VANISH);
785             }
786             avatarRange.setLastMatchedGroupRange(getMatcher(), 1);
787
788             shrinkRegion();
789
790             this.sysEventHandler
791                 .sysEventVanish(getContent(), avatarRange);
792         }
793
794         if( ! hasVanish ){
795             popRegion();
796             return false;
797         }
798
799         sweepSpace();
800
801         return true;
802     }
803
804     private static final Pattern CHECKOUT_PATTERN =
805             compile(
806                  "(?:<br />)*"
807                 +"(" + AVATAR_REGEX + ")"
808                 +"\u0020は、宿を去った。"
809                 +"(?:<br />)*"
810             );
811
812     /**
813      * CHECKOUTメッセージのパースを試みる。
814      * @return マッチしたらtrue
815      * @throws HtmlParseException パースエラー
816      */
817     private boolean probeCheckout() throws HtmlParseException{
818         SeqRange avatarRange  = this.rangepool_1;
819
820         pushRegion();
821
822         sweepSpace();
823
824         boolean hasCheckout = false;
825
826         for(;;){
827             if( ! lookingAtProbe(CHECKOUT_PATTERN)){
828                 break;
829             }
830
831             if( ! hasCheckout ){
832                 hasCheckout = true;
833                 this.sysEventHandler.sysEventType(SysEventType.CHECKOUT);
834             }
835             avatarRange.setLastMatchedGroupRange(getMatcher(), 1);
836
837             shrinkRegion();
838
839             this.sysEventHandler
840                 .sysEventCheckout(getContent(), avatarRange);
841         }
842
843         if( ! hasCheckout ){
844             popRegion();
845             return false;
846         }
847
848         sweepSpace();
849
850         return true;
851     }
852
853     /**
854      * Orderメッセージをパースする。
855      * @throws HtmlParseException パースエラー
856      */
857     public void parseOrder() throws HtmlParseException{
858         setContextErrorMessage("Unknown Order message");
859
860         this.sysEventHandler.startSysEvent(EventFamily.ORDER);
861
862         int regionStart = regionStart();
863         int regionEnd   = regionEnd();
864
865         boolean result =
866                    probeAskEntry()
867                 || probeAskCommit()
868                 || probeNoComment()
869                 || probeStayEpilogue()
870                 || probeGameOver()
871                 ;
872         if( ! result ){
873             throw buildParseException();
874         }
875
876         getMatcher().region(regionStart, regionEnd);
877         parseContent();
878
879         lookingAtAffirm(C_DIV_PATTERN);
880         shrinkRegion();
881
882         this.sysEventHandler.endSysEvent();
883
884         return;
885     }
886
887     private static final Pattern ASKENTRY_PATTERN =
888             compile(
889              "演じたいキャラクターを選び、発言してください。<br />"
890             +"([0-2][0-9]):([0-5][0-9])\u0020に"
891             +"([0-9]+)名以上がエントリーしていれば進行します。<br />"
892             +"最大([0-9]+)名まで参加可能です。<br /><br />"
893             +"※[\u0020]?エントリーは取り消せません。"
894             +"ルールをよく理解した上でご参加下さい。<br />"
895             +"(?:※始めての方は、村人希望での参加となります。<br />)?"
896             +"(?:※希望能力についての発言は控えてください。<br />)?"
897             );
898
899     /**
900      * ASKENTRYメッセージのパースを試みる。
901      * @return マッチしたらtrue
902      * @throws HtmlParseException パースエラー
903      */
904     private boolean probeAskEntry() throws HtmlParseException{
905         pushRegion();
906
907         sweepSpace();
908
909         if( ! lookingAtProbe(ASKENTRY_PATTERN)){
910             popRegion();
911             return false;
912         }
913
914         int hour     = parseGroupedInt(1);
915         int minute   = parseGroupedInt(2);
916         int minLimit = parseGroupedInt(3);
917         int maxLimit = parseGroupedInt(4);
918
919         shrinkRegion();
920
921         this.sysEventHandler.sysEventType(SysEventType.ASKENTRY);
922         this.sysEventHandler
923             .sysEventAskEntry(hour, minute, minLimit, maxLimit);
924
925         sweepSpace();
926
927         return true;
928     }
929
930     private static final Pattern ASKCOMMIT_PATTERN =
931             compile(
932              "(?:"
933             +"([0-2][0-9]):([0-5][0-9])\u0020までに、"
934             +"誰を処刑するべきかの投票先を決定して下さい。<br />"
935             +"一番票を集めた人物が処刑されます。"
936             +"同数だった場合はランダムで決定されます。<br /><br />"
937             +")?"
938             +"特殊な能力を持つ人は、"
939             +"([0-2][0-9]):([0-5][0-9])\u0020までに"
940             +"行動を確定して下さい。<br />"
941             );
942
943     /**
944      * ASKCOMMITメッセージのパースを試みる。
945      * @return マッチしたらtrue
946      * @throws HtmlParseException パースエラー
947      */
948     private boolean probeAskCommit() throws HtmlParseException{
949         pushRegion();
950
951         sweepSpace();
952
953         if( ! lookingAtProbe(ASKCOMMIT_PATTERN)){
954             popRegion();
955             return false;
956         }
957
958         boolean is1stDay;
959         if(isGroupMatched(1)){
960             is1stDay = false;
961         }else{
962             is1stDay = true;
963         }
964
965         int hh1 = parseGroupedInt(1);
966         int mm1 = parseGroupedInt(2);
967         int hh2 = parseGroupedInt(3);
968         int mm2 = parseGroupedInt(4);
969
970         shrinkRegion();
971
972         if( ! is1stDay && (hh1 != hh2 || mm1 != mm2) ){
973             throw new HtmlParseException(regionStart());
974         }
975
976         this.sysEventHandler.sysEventType(SysEventType.ASKCOMMIT);
977         this.sysEventHandler.sysEventAskCommit(hh2, mm2);
978
979         sweepSpace();
980
981         return true;
982     }
983
984     private static final Pattern NOCOMMENT_HEAD_PATTERN =
985             compile("本日まだ発言していない者は、");
986     private static final Pattern NOCOMMENT_AVATAR_PATTERN =
987             compile(
988              "(?:"
989                 +"(" + AVATAR_REGEX + ")、"
990             +")|(?:"
991                 +"以上\u0020([0-9]+)\u0020名。"
992             +")"
993             );
994
995     /**
996      * NOCOMMENTメッセージのパースを試みる。
997      * @return マッチしたらtrue
998      * @throws HtmlParseException パースエラー
999      */
1000     private boolean probeNoComment() throws HtmlParseException{
1001         SeqRange avatarRange = this.rangepool_1;
1002
1003         pushRegion();
1004
1005         sweepSpace();
1006
1007         if( ! lookingAtProbe(NOCOMMENT_HEAD_PATTERN)){
1008             popRegion();
1009             return false;
1010         }
1011         shrinkRegion();
1012
1013         this.sysEventHandler.sysEventType(SysEventType.NOCOMMENT);
1014
1015         int avatarNum = 0;
1016         for(;;){
1017             if( ! lookingAtProbe(NOCOMMENT_AVATAR_PATTERN)){
1018                 popRegion();
1019                 return false;
1020             }
1021
1022             if(isGroupMatched(1)){
1023                 avatarRange.setLastMatchedGroupRange(getMatcher(), 1);
1024                 this.sysEventHandler
1025                     .sysEventNoComment(getContent(), avatarRange);
1026                 shrinkRegion();
1027                 avatarNum++;
1028             }else if(isGroupMatched(2)){
1029                 int num = parseGroupedInt(2);
1030                 shrinkRegion();
1031                 if(num != avatarNum){
1032                     throw new HtmlParseException(regionStart());
1033                 }
1034                 break;
1035             }
1036         }
1037
1038         sweepSpace();
1039
1040         return true;
1041     }
1042
1043     private static final Pattern STAYEPILOGUE_PATTERN =
1044             compile(
1045             "(?:(村人)|(人狼)|(ハムスター))側の勝利です!<br />"
1046             +"全てのログとユーザー名を公開します。"
1047             +"([0-2][0-9]):([0-5][0-9])\u0020まで"
1048             +"自由に書き込めますので、"
1049             +"今回の感想などをどうぞ。<br />"
1050             );
1051
1052     /**
1053      * STAYEPILOGUEメッセージのパースを試みる。
1054      * @return マッチしたらtrue
1055      * @throws HtmlParseException パースエラー
1056      */
1057     private boolean probeStayEpilogue() throws HtmlParseException{
1058         pushRegion();
1059
1060         sweepSpace();
1061
1062         if( ! lookingAtProbe(STAYEPILOGUE_PATTERN)){
1063             popRegion();
1064             return false;
1065         }
1066
1067         Team winner = null;
1068         if(isGroupMatched(1)){
1069             winner = Team.VILLAGE;
1070         }else if(isGroupMatched(2)){
1071             winner = Team.WOLF;
1072         }else if(isGroupMatched(3)){
1073             winner = Team.HAMSTER;
1074         }
1075
1076         int hour = parseGroupedInt(4);
1077         int minute = parseGroupedInt(5);
1078
1079         shrinkRegion();
1080
1081         this.sysEventHandler.sysEventType(SysEventType.STAYEPILOGUE);
1082         this.sysEventHandler.sysEventStayEpilogue(winner, hour, minute);
1083
1084         sweepSpace();
1085
1086         return true;
1087     }
1088
1089     private static final Pattern GAMEOVER_PATTERN =
1090             compile("終了しました。" + "<br />");
1091
1092     /**
1093      * GAMEOVERメッセージのパースを試みる。
1094      * @return マッチしたらtrue
1095      * @throws HtmlParseException パースエラー
1096      */
1097     private boolean probeGameOver() throws HtmlParseException{
1098         pushRegion();
1099
1100         sweepSpace();
1101
1102         if( ! lookingAtProbe(GAMEOVER_PATTERN)){
1103             popRegion();
1104             return false;
1105         }
1106
1107         shrinkRegion();
1108
1109         this.sysEventHandler.sysEventType(SysEventType.GAMEOVER);
1110
1111         sweepSpace();
1112
1113         return true;
1114     }
1115
1116     /**
1117      * Extraメッセージをパースする。
1118      * @throws HtmlParseException パースエラー
1119      */
1120     public void parseExtra() throws HtmlParseException{
1121         setContextErrorMessage("Unknown Extra message");
1122
1123         this.sysEventHandler.startSysEvent(EventFamily.EXTRA);
1124
1125         int regionStart = regionStart();
1126         int regionEnd   = regionEnd();
1127
1128         boolean result =
1129                    probeJudge()
1130                 || probeGuard()
1131                 || probeCounting2();
1132         if( ! result ){
1133             throw buildParseException();
1134         }
1135
1136         getMatcher().region(regionStart, regionEnd);
1137         parseContent();
1138
1139         lookingAtAffirm(C_DIV_PATTERN);
1140         shrinkRegion();
1141
1142         this.sysEventHandler.endSysEvent();
1143
1144         return;
1145     }
1146
1147     private static final Pattern JUDGE_DELIM_PATTERN =
1148             compile("\u0020は、");
1149     private static final Pattern JUDGE_TAIL_PATTERN =
1150             compile("\u0020を占った。");
1151
1152     /**
1153      * JUDGEメッセージのパースを試みる。
1154      * @return マッチしたらtrue
1155      * @throws HtmlParseException パースエラー
1156      */
1157     private boolean probeJudge() throws HtmlParseException{
1158         SeqRange judgeByRange = this.rangepool_1;
1159         SeqRange judgeToRange = this.rangepool_2;
1160
1161         pushRegion();
1162
1163         sweepSpace();
1164
1165         if( ! lookingAtProbe(AVATAR_PATTERN)){
1166             popRegion();
1167             return false;
1168         }
1169         judgeByRange.setLastMatchedRange(getMatcher());
1170         shrinkRegion();
1171
1172         if( ! lookingAtProbe(JUDGE_DELIM_PATTERN)){
1173             popRegion();
1174             return false;
1175         }
1176         shrinkRegion();
1177
1178         if( ! lookingAtProbe(AVATAR_PATTERN)){
1179             popRegion();
1180             return false;
1181         }
1182         judgeToRange.setLastMatchedRange(getMatcher());
1183         shrinkRegion();
1184
1185         if( ! lookingAtProbe(JUDGE_TAIL_PATTERN)){
1186             popRegion();
1187             return false;
1188         }
1189         shrinkRegion();
1190
1191         this.sysEventHandler.sysEventType(SysEventType.JUDGE);
1192         this.sysEventHandler
1193             .sysEventJudge(getContent(),
1194                            judgeByRange,
1195                            judgeToRange );
1196         sweepSpace();
1197
1198         return true;
1199     }
1200
1201     private static final Pattern GUARD_DELIM_PATTERN =
1202             compile("\u0020は、");
1203     private static final Pattern GUARD_TAIL_PATTERN =
1204             compile("\u0020を守っている。");
1205
1206     /**
1207      * GUARDメッセージのパースを試みる。
1208      * @return マッチしたらtrue
1209      * @throws HtmlParseException パースエラー
1210      */
1211     private boolean probeGuard() throws HtmlParseException{
1212         SeqRange guardByRange = this.rangepool_1;
1213         SeqRange guardToRange = this.rangepool_2;
1214
1215         pushRegion();
1216
1217         sweepSpace();
1218
1219         if( ! lookingAtProbe(AVATAR_PATTERN)){
1220             popRegion();
1221             return false;
1222         }
1223         guardByRange.setLastMatchedRange(getMatcher());
1224         shrinkRegion();
1225
1226         if( ! lookingAtProbe(GUARD_DELIM_PATTERN)){
1227             popRegion();
1228             return false;
1229         }
1230         shrinkRegion();
1231
1232         if( ! lookingAtProbe(AVATAR_PATTERN)){
1233             popRegion();
1234             return false;
1235         }
1236         guardToRange.setLastMatchedRange(getMatcher());
1237         shrinkRegion();
1238
1239         if( ! lookingAtProbe(GUARD_TAIL_PATTERN)){
1240             popRegion();
1241             return false;
1242         }
1243         shrinkRegion();
1244
1245         this.sysEventHandler.sysEventType(SysEventType.GUARD);
1246         this.sysEventHandler.sysEventGuard(getContent(),
1247                                            guardByRange,
1248                                            guardToRange );
1249         sweepSpace();
1250
1251         return true;
1252     }
1253
1254     private static final Pattern CONTENT_PATTERN =
1255             compile(
1256              "("
1257                 +"[^<>\\n\\r]+"
1258             +")|("
1259                 +"<br />"
1260             +")|(?:"
1261                 +"<a\u0020href=\"([^\"]*)\">([^<>]*)</a>"
1262             +")"
1263             );
1264
1265     /**
1266      * システムイベントの内容文字列をパースする。
1267      * @throws HtmlParseException パースエラー
1268      */
1269     private void parseContent() throws HtmlParseException{
1270         SeqRange anchorRange  = this.rangepool_1;
1271         SeqRange contentRange = this.rangepool_2;
1272
1273         sweepSpace();
1274
1275         for(;;){
1276             if( ! lookingAtProbe(CONTENT_PATTERN) ){
1277                 break;
1278             }
1279
1280             if(isGroupMatched(1)){
1281                 contentRange.setLastMatchedGroupRange(getMatcher(), 1);
1282                 this.sysEventHandler
1283                     .sysEventContent(getContent(), contentRange);
1284             }else if(isGroupMatched(2)){
1285                 this.sysEventHandler.sysEventContentBreak();
1286             }else if(isGroupMatched(3)){
1287                 anchorRange.setLastMatchedGroupRange(getMatcher(), 3);
1288                 contentRange.setLastMatchedGroupRange(getMatcher(), 4);
1289                 this.sysEventHandler
1290                     .sysEventContentAnchor(getContent(),
1291                                            anchorRange,
1292                                            contentRange );
1293             }
1294
1295             shrinkRegion();
1296         }
1297
1298         sweepSpace();
1299
1300         return;
1301     }
1302
1303     /**
1304      * 一時的に現在の検索領域を待避する。
1305      * 待避できるのは1回のみ。複数回スタックはできない。
1306      * @see #popRegion()
1307      */
1308     private void pushRegion(){
1309         this.pushedRegionStart = regionStart();
1310         this.pushedRegionEnd   = regionEnd();
1311         return;
1312     }
1313
1314     /**
1315      * 一時的に待避した検索領域を復活させる。
1316      * @throws IllegalStateException まだ何も待避していない。
1317      * @see #pushRegion()
1318      */
1319     private void popRegion() throws IllegalStateException{
1320         if(this.pushedRegionStart < 0 || this.pushedRegionEnd < 0){
1321             throw new IllegalStateException();
1322         }
1323
1324         if(    this.pushedRegionStart != regionStart()
1325             || this.pushedRegionEnd   != regionEnd()  ){
1326             getMatcher().region(this.pushedRegionStart, this.pushedRegionEnd);
1327         }
1328
1329         this.pushedRegionStart = -1;
1330         this.pushedRegionEnd   = -1;
1331
1332         return;
1333     }
1334
1335 }