OSDN Git Service

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