OSDN Git Service

Maven3対応。
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / summary / GameSummary.java
1 /*
2  * Summarize game information
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.summary;
9
10 import java.net.MalformedURLException;
11 import java.net.URI;
12 import java.net.URISyntaxException;
13 import java.net.URL;
14 import java.text.DateFormat;
15 import java.util.Collections;
16 import java.util.Comparator;
17 import java.util.Date;
18 import java.util.EnumMap;
19 import java.util.HashMap;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24 import jp.sfjp.jindolf.VerInfo;
25 import jp.sfjp.jindolf.data.Avatar;
26 import jp.sfjp.jindolf.data.Period;
27 import jp.sfjp.jindolf.data.Player;
28 import jp.sfjp.jindolf.data.SysEvent;
29 import jp.sfjp.jindolf.data.Talk;
30 import jp.sfjp.jindolf.data.Topic;
31 import jp.sfjp.jindolf.data.Village;
32 import jp.sfjp.jindolf.dxchg.FaceIconSet;
33 import jp.sfjp.jindolf.dxchg.WolfBBS;
34 import jp.sourceforge.jindolf.corelib.Destiny;
35 import jp.sourceforge.jindolf.corelib.GameRole;
36 import jp.sourceforge.jindolf.corelib.SysEventType;
37 import jp.sourceforge.jindolf.corelib.Team;
38 import jp.sourceforge.jindolf.corelib.VillageState;
39
40 /**
41  * 決着の付いたゲームのサマリを集計。
42  */
43 public class GameSummary{
44
45     /** キャスティング表示用Comparator。 */
46     public static final Comparator<Player> COMPARATOR_CASTING =
47             new CastingComparator();
48
49     private static final String GENERATOR =
50             VerInfo.TITLE + "\u0020Ver." + VerInfo.VERSION;
51
52
53     private final Map<Avatar, Player> playerMap =
54             new HashMap<Avatar, Player>();
55     private final List<Player> playerList =
56             new LinkedList<Player>();
57     private final Map<SysEventType, List<SysEvent>> eventMap =
58             new EnumMap<SysEventType, List<SysEvent>>(SysEventType.class);
59
60     private final Village village;
61
62     // 勝者
63     private Team winner;
64
65     // 占い先集計
66     private int ctScryVillage = 0;
67     private int ctScryHamster = 0;
68     private int ctScryMadman  = 0;
69     private int ctScryWolf    = 0;
70
71     // 護衛先集計
72     private int ctGuardVillage   = 0;
73     private int ctGuardHamster   = 0;
74     private int ctGuardMadman    = 0;
75     private int ctGuardWolf      = 0;
76     private int ctGuardVillageGJ = 0;
77     private int ctGuardHamsterGJ = 0;
78     private int ctGuardMadmanGJ  = 0;
79     private int ctGuardFakeGJ    = 0;
80
81     // 発言時刻範囲
82     private long talk1stTimeMs = -1;
83     private long talkLastTimeMs = -1;
84
85
86     /**
87      * コンストラクタ。
88      * @param village 村
89      */
90     public GameSummary(Village village){
91         super();
92
93         VillageState state = village.getState();
94         if(   state != VillageState.EPILOGUE
95            && state != VillageState.GAMEOVER){
96             throw new IllegalStateException();
97         }
98
99         this.village = village;
100
101         summarize();
102
103         return;
104     }
105
106
107     /**
108      * プレイヤーのリストから役職バランス文字列を得る。
109      * ex) "村村占霊狂狼"
110      * @param players プレイヤーのリスト
111      * @return 役職バランス文字列
112      */
113     public static String getRoleBalanceSequence(List<Player> players){
114         List<GameRole> roleList = new LinkedList<GameRole>();
115         for(Player player : players){
116             GameRole role = player.getRole();
117             roleList.add(role);
118         }
119         Collections.sort(roleList, GameRole.getPowerBalanceComparator());
120
121         StringBuilder result = new StringBuilder();
122         for(GameRole role : roleList){
123             char ch = role.getShortName();
124             result.append(ch);
125         }
126
127         return result.toString();
128     }
129
130     /**
131      * サマライズ処理。
132      */
133     private void summarize(){
134         buildEventMap();
135
136         summarizeTime();
137         summarizeWinner();
138         summarizePlayers();
139
140         for(Period period : this.village.getPeriodList()){
141             summarizePeriod(period);
142         }
143
144         summarizeJudge();
145         summarizeGuard();
146
147         return;
148     }
149
150     /**
151      * SysEventの種別ごとに集計する。
152      */
153     private void buildEventMap(){
154         for(SysEventType type : SysEventType.values()){
155             List<SysEvent> eventList = new LinkedList<SysEvent>();
156             this.eventMap.put(type, eventList);
157         }
158
159         for(Period period : this.village.getPeriodList()){
160             for(Topic topic : period.getTopicList()){
161                 if( ! (topic instanceof SysEvent) ) continue;
162                 SysEvent event = (SysEvent) topic;
163                 SysEventType type = event.getSysEventType();
164                 List<SysEvent> eventList = this.eventMap.get(type);
165                 eventList.add(event);
166             }
167         }
168
169         return;
170     }
171
172     /**
173      * 勝者集計。
174      */
175     private void summarizeWinner(){
176         List<SysEvent> eventList;
177
178         eventList = this.eventMap.get(SysEventType.WINVILLAGE);
179         if( ! eventList.isEmpty() ){
180             this.winner = Team.VILLAGE;
181         }
182
183         eventList = this.eventMap.get(SysEventType.WINWOLF);
184         if(  ! eventList.isEmpty() ){
185             this.winner = Team.WOLF;
186         }
187
188         eventList = this.eventMap.get(SysEventType.WINHAMSTER);
189         if(  ! eventList.isEmpty() ){
190             this.winner = Team.HAMSTER;
191         }
192
193         if(this.winner == null) assert false;
194
195         return;
196     }
197
198     /**
199      * 参加者集計。
200      */
201     private void summarizePlayers(){
202         List<SysEvent> eventList;
203
204         List<Avatar>       avatarList;
205         List<GameRole>     roleList;
206         List<Integer>      integerList;
207         List<CharSequence> textList;
208
209         eventList = this.eventMap.get(SysEventType.ONSTAGE);
210         for(SysEvent event : eventList){
211             avatarList  = event.getAvatarList();
212             integerList = event.getIntegerList();
213             Avatar onstageAvatar = avatarList.get(0);
214             Player onstagePlayer = registPlayer(onstageAvatar);
215             onstagePlayer.setEntryNo(integerList.get(0));
216         }
217
218         eventList = this.eventMap.get(SysEventType.PLAYERLIST);
219         assert eventList.size() == 1;
220         SysEvent event = eventList.get(0);
221
222         avatarList  = event.getAvatarList();
223         roleList    = event.getRoleList();
224         integerList = event.getIntegerList();
225         textList    = event.getCharSequenceList();
226         int avatarNum = avatarList.size();
227         for(int idx = 0; idx < avatarNum; idx++){
228             Avatar avatar = avatarList.get(idx);
229             GameRole role = roleList.get(idx);
230             CharSequence urlText = textList.get(idx * 2);
231             CharSequence idName  = textList.get(idx * 2 + 1);
232             int liveOrDead = integerList.get(idx);
233
234             Player player = registPlayer(avatar);
235             player.setRole(role);
236             player.setUrlText(urlText.toString());
237             player.setIdName(idName.toString());
238             if(liveOrDead != 0){        // 生存
239                 player.setObitDay(-1);
240                 player.setDestiny(Destiny.ALIVE);
241             }
242
243             this.playerList.add(player);
244         }
245
246         return;
247     }
248
249     /**
250      * Periodのサマライズ。
251      * @param period Period
252      */
253     private void summarizePeriod(Period period){
254         int day = period.getDay();
255         for(Topic topic : period.getTopicList()){
256             if(topic instanceof SysEvent){
257                 SysEvent sysEvent = (SysEvent) topic;
258                 summarizeDestiny(day, sysEvent);
259             }
260         }
261
262         return;
263     }
264
265     /**
266      * 各プレイヤー運命のサマライズ。
267      * @param day 日
268      * @param sysEvent システムイベント
269      */
270     private void summarizeDestiny(int day, SysEvent sysEvent){
271         List<Avatar>  avatarList  = sysEvent.getAvatarList();
272         List<Integer> integerList = sysEvent.getIntegerList();
273
274         int avatarTotal = avatarList.size();
275         Avatar lastAvatar = null;
276         if(avatarTotal > 0) lastAvatar = avatarList.get(avatarTotal - 1);
277
278         SysEventType eventType = sysEvent.getSysEventType();
279         switch(eventType){
280         case EXECUTION:  // G国のみ
281             if(integerList.get(avatarTotal - 1) > 0) break;  // 処刑無し
282             Player executedPl = registPlayer(lastAvatar);
283             executedPl.setDestiny(Destiny.EXECUTED);
284             executedPl.setObitDay(day);
285             break;
286         case SUDDENDEATH:
287             Avatar suddenDeathAvatar = avatarList.get(0);
288             Player suddenDeathPlayer = registPlayer(suddenDeathAvatar);
289             suddenDeathPlayer.setDestiny(Destiny.SUDDENDEATH);
290             suddenDeathPlayer.setObitDay(day);
291             break;
292         case COUNTING:  // G国COUNTING2は運命に関係なし
293             if(avatarTotal % 2 == 0) break;  // 処刑無し
294             Player executedPlayer = registPlayer(lastAvatar);
295             executedPlayer.setDestiny(Destiny.EXECUTED);
296             executedPlayer.setObitDay(day);
297             break;
298         case MURDERED:
299             for(Avatar avatar : avatarList){
300                 Player player = registPlayer(avatar);
301                 player.setDestiny(Destiny.EATEN);
302                 player.setObitDay(day);
303             }
304             // TODO E国ハム溶け処理は後回し
305             break;
306         default:
307             break;
308         }
309
310         return;
311     }
312
313     /**
314      * 会話時刻のサマライズ。
315      */
316     private void summarizeTime(){
317         for(Period period : this.village.getPeriodList()){
318             for(Topic topic : period.getTopicList()){
319                 if( ! (topic instanceof Talk) ) continue;
320                 Talk talk = (Talk) topic;
321
322                 long epoch = talk.getTimeFromID();
323
324                 if(this.talk1stTimeMs  < 0) this.talk1stTimeMs  = epoch;
325                 if(this.talkLastTimeMs < 0) this.talkLastTimeMs = epoch;
326
327                 if(epoch < this.talk1stTimeMs ) this.talk1stTimeMs  = epoch;
328                 if(epoch > this.talkLastTimeMs) this.talkLastTimeMs = epoch;
329             }
330         }
331
332         return;
333     }
334
335     /**
336      * 占い師の活動を集計する。
337      */
338     private void summarizeJudge(){
339         List<SysEvent> eventList = this.eventMap.get(SysEventType.JUDGE);
340
341         for(SysEvent event : eventList){
342             List<Avatar> avatarList  = event.getAvatarList();
343             Avatar avatar = avatarList.get(1);
344             Player seered = getPlayer(avatar);
345             GameRole role = seered.getRole();
346             switch(role){
347             case WOLF:    this.ctScryWolf++;    break;
348             case MADMAN:  this.ctScryMadman++;  break;
349             case HAMSTER: this.ctScryHamster++; break;
350             default:      this.ctScryVillage++; break;
351             }
352         }
353
354         return;
355     }
356
357     /**
358      * 占い師の活動を文字列化する。
359      * @return 占い師の活動
360      */
361     public CharSequence dumpSeerActivity(){
362         StringBuilder result = new StringBuilder();
363
364         if(this.ctScryVillage > 0){
365             result.append("村陣営を");
366             result.append(this.ctScryVillage);
367             result.append("回");
368         }
369
370         if(this.ctScryHamster > 0){
371             if(result.length() > 0) result.append('、');
372             result.append("ハムスターを");
373             result.append(this.ctScryHamster);
374             result.append("回");
375         }
376
377         if(this.ctScryMadman > 0){
378             if(result.length() > 0) result.append('、');
379             result.append("狂人を");
380             result.append(this.ctScryMadman);
381             result.append("回");
382         }
383
384         if(this.ctScryWolf > 0){
385             if(result.length() > 0) result.append('、');
386             result.append("人狼を");
387             result.append(this.ctScryWolf);
388             result.append("回");
389         }
390
391         if(result.length() <= 0) result.append("誰も占わなかった。");
392         else                     result.append("占った。");
393
394         CharSequence seq = WolfBBS.escapeWikiSyntax(result);
395
396         return seq;
397     }
398
399     /**
400      * 狩人の活動を集計する。
401      */
402     private void summarizeGuard(){
403         List<SysEvent> eventList;
404
405         eventList = this.eventMap.get(SysEventType.GUARD);
406         for(SysEvent event : eventList){
407             List<Avatar> avatarList = event.getAvatarList();
408             Avatar avatar = avatarList.get(1);
409             Player guarded = getPlayer(avatar);
410             GameRole guardedRole = guarded.getRole();
411             switch(guardedRole){
412             case WOLF:    this.ctGuardWolf++;    break;
413             case MADMAN:  this.ctGuardMadman++;  break;
414             case HAMSTER: this.ctGuardHamster++; break;
415             default:      this.ctGuardVillage++; break;
416             }
417         }
418
419         for(Period period : this.village.getPeriodList()){
420             summarizeGjPeriod(period);
421         }
422
423         return;
424     }
425
426     /**
427      * 狩人GJの日ごとの集計。
428      * @param period 日
429      */
430     private void summarizeGjPeriod(Period period){
431         if(period.getDay() <= 2) return;
432
433         boolean hasAssaultTried = period.hasAssaultTried();
434         boolean hunterAlive = false;
435         int wolfNum = 0;
436
437         Set<Avatar> voters = period.getVoterSet();
438         for(Avatar avatar : voters){
439             Player player = getPlayer(avatar);
440             switch(player.getRole()){
441             case HUNTER: hunterAlive = true; break;
442             case WOLF:   wolfNum++;          break;
443             default:                         break;
444             }
445         }
446
447         Avatar executed = period.getExecutedAvatar();
448         if(executed != null){
449             Player player = getPlayer(executed);
450             switch(player.getRole()){
451             case HUNTER: hunterAlive = false; break;
452             case WOLF:   wolfNum--;           break;
453             default:                          break;
454             }
455         }
456
457         if( ! hunterAlive || wolfNum <= 0) return;
458
459         SysEvent sysEvent;
460
461         sysEvent = period.getTypedSysEvent(SysEventType.NOMURDER);
462         if(sysEvent == null) return;
463
464         sysEvent = period.getTypedSysEvent(SysEventType.GUARD);
465         if(sysEvent == null) return;
466
467         if(hasAssaultTried){
468             Avatar guarded = sysEvent.getAvatarList().get(1);
469             Player guardedPlayer = getPlayer(guarded);
470             GameRole guardedRole = guardedPlayer.getRole();
471             switch(guardedRole){
472             case MADMAN:  this.ctGuardMadmanGJ++;   break;
473             case HAMSTER: this.ctGuardHamsterGJ++;  break;
474             default:      this.ctGuardVillageGJ++;  break;
475             }
476         }else{
477             this.ctGuardFakeGJ++;   // 偽装GJ
478         }
479
480         return;
481     }
482
483     /**
484      * 狩人の活動を文字列化する。
485      * @return 狩人の活動
486      */
487     public CharSequence dumpHunterActivity(){
488         StringBuilder result = new StringBuilder();
489
490         String atLeast;
491         if(this.ctGuardFakeGJ > 0) atLeast = "少なくとも";
492         else                       atLeast = "";
493
494         if(this.ctGuardVillage > 0){
495             result.append(atLeast);
496             result.append("村陣営を");
497             result.append(this.ctGuardVillage);
498             result.append("回護衛し");
499             if(this.ctGuardVillageGJ > 0){
500                 result.append("GJを");
501                 result.append(this.ctGuardVillageGJ);
502                 result.append("回出した。");
503             }else{
504                 result.append("た。");
505             }
506         }
507
508         if(this.ctGuardHamster > 0){
509             result.append(atLeast);
510             result.append("ハムスターを");
511             result.append(this.ctGuardHamster);
512             result.append("回護衛し");
513             if(this.ctGuardHamsterGJ > 0){
514                 result.append("GJを");
515                 result.append(this.ctGuardHamsterGJ);
516                 result.append("回出した。");
517             }else{
518                 result.append("た。");
519             }
520         }
521
522         if(this.ctGuardMadman > 0){
523             result.append(atLeast);
524             result.append("狂人を");
525             result.append(this.ctGuardMadman);
526             result.append("回護衛し");
527             if(this.ctGuardMadmanGJ > 0){
528                 result.append("GJを");
529                 result.append(this.ctGuardMadmanGJ);
530                 result.append("回出した。");
531             }else{
532                 result.append("た。");
533             }
534         }
535
536         if(this.ctGuardWolf > 0){
537             result.append(atLeast);
538             result.append("人狼を");
539             result.append(this.ctGuardWolf);
540             result.append("回護衛した。");
541         }
542
543         if(this.ctGuardFakeGJ > 0){
544             result.append("護衛先は不明ながら偽装GJが");
545             result.append(this.ctGuardFakeGJ);
546             result.append("回あった。");
547         }
548
549         if(result.length() <= 0) result.append("誰も護衛できなかった");
550
551         CharSequence seq = WolfBBS.escapeWikiSyntax(result);
552
553         return seq;
554     }
555
556     /**
557      * 処刑概観を文字列化する。
558      * @return 文字列化した処刑概観
559      */
560     public CharSequence dumpExecutionInfo(){
561         StringBuilder result = new StringBuilder();
562
563         int exeWolf = 0;
564         int exeMad = 0;
565         int exeVillage = 0;
566         for(Player player : this.playerList){
567             Destiny destiny = player.getDestiny();
568             if(destiny != Destiny.EXECUTED) continue;
569             GameRole role = player.getRole();
570             switch(role){
571             case WOLF:   exeWolf++;    break;
572             case MADMAN: exeMad++;     break;
573             default:     exeVillage++; break;
574             }
575         }
576
577         if(exeVillage > 0){
578             result.append("▼村陣営×").append(exeVillage).append("回");
579         }
580         if(exeMad > 0){
581             if(result.length() > 0) result.append("、");
582             result.append("▼狂×").append(exeMad).append("回");
583         }
584         if(exeWolf > 0){
585             if(result.length() > 0) result.append("、");
586             result.append("▼狼×").append(exeWolf).append("回");
587         }
588         if(result.length() <= 0) result.append("なし");
589
590         CharSequence seq = WolfBBS.escapeWikiSyntax(result);
591
592         return seq;
593     }
594
595     /**
596      * 襲撃概観を文字列化する。
597      * @return 文字列化した襲撃概観
598      */
599     public CharSequence dumpAssaultInfo(){
600         StringBuilder result = new StringBuilder();
601
602         int eatMad = 0;
603         int eatVillage = 0;
604         for(Player player : this.playerList){
605             if(player.getAvatar() == Avatar.AVATAR_GERD){
606                 result.append("▲ゲルト");
607                 continue;
608             }
609             Destiny destiny = player.getDestiny();
610             if(destiny != Destiny.EATEN) continue;
611             GameRole role = player.getRole();
612             switch(role){
613             case MADMAN: eatMad++;     break;
614             default:     eatVillage++; break;
615             }
616         }
617
618         if(eatVillage > 0){
619             if(result.length() > 0) result.append("、");
620             result.append("▲村陣営×").append(eatVillage).append("回");
621         }
622         if(eatMad > 0){
623             if(result.length() > 0) result.append("、");
624             result.append("▲狂×").append(eatMad).append("回");
625         }
626
627         if(result.length() <= 0) result.append("襲撃なし");
628
629         CharSequence seq = WolfBBS.escapeWikiSyntax(result);
630
631         return seq;
632     }
633
634     /**
635      * まとめサイト用投票Boxを生成する。
636      * @return 投票BoxのWikiテキスト
637      */
638     public CharSequence dumpVoteBox(){
639         StringBuilder wikiText = new StringBuilder();
640
641         for(Player player : getCastingPlayerList()){
642             Avatar avatar = player.getAvatar();
643             if(avatar == Avatar.AVATAR_GERD) continue;
644             GameRole role = player.getRole();
645             CharSequence fullName = avatar.getFullName();
646             CharSequence roleName = role.getRoleName();
647             StringBuilder line = new StringBuilder();
648             line.append("[").append(roleName).append("] ").append(fullName);
649             if(wikiText.length() > 0) wikiText.append(',');
650             wikiText.append(WolfBBS.escapeWikiSyntax(line));
651             wikiText.append("[0]");
652         }
653
654         wikiText.insert(0, "#vote(").append(")\n");
655
656         return wikiText;
657     }
658
659     /**
660      * まとめサイト用キャスト表を生成する。
661      * @param iconSet 顔アイコンセット
662      * @return キャスト表のWikiテキスト
663      */
664     public CharSequence dumpCastingBoard(FaceIconSet iconSet){
665         StringBuilder wikiText = new StringBuilder();
666
667         String vName = this.village.getVillageFullName();
668         String author = iconSet.getAuthor() + "氏"
669                        +" [ "+iconSet.getUrlText()+" ]";
670
671         wikiText.append(WolfBBS.COMMENTLINE);
672         wikiText.append("// ↓キャスト表開始\n");
673         wikiText.append("//        Village : ")
674                 .append(vName)
675                 .append('\n');
676         wikiText.append("//        Generator : ")
677                 .append(GENERATOR)
678                 .append('\n');
679         wikiText.append("//        アイコン作者 : ")
680                 .append(author)
681                 .append('\n');
682         wikiText.append("// ※アイコン画像の著作財産権保持者")
683                 .append("および画像サーバ運営者から\n");
684         wikiText.append("// 新しい意向が示された場合、")
685                 .append("そちらを最優先で尊重してください。\n");
686         wikiText.append(WolfBBS.COMMENTLINE);
687
688         wikiText.append("|配役")
689                 .append("|参加者")
690                 .append("|役職")
691                 .append("|運命")
692                 .append("|その活躍")
693                 .append("|h")
694                 .append('\n');
695         wikiText.append(WolfBBS.COMMENTLINE);
696
697         for(Player player : getCastingPlayerList()){
698             Avatar avatar   = player.getAvatar();
699             GameRole role   = player.getRole();
700             Destiny destiny = player.getDestiny();
701             int obitDay     = player.getObitDay();
702             String name     = player.getIdName();
703             String urlText  = player.getUrlText();
704             if(urlText == null) urlText = "";
705             urlText = urlText.replace("~", "%7e");
706             urlText = urlText.replace(" ", "%20");
707             try{
708                 URL url = new URL(urlText);
709                 URI uri = url.toURI();
710                 urlText = uri.toASCIIString();
711             }catch(MalformedURLException e){
712                 // NOTHING
713             }catch(URISyntaxException e){
714                 // NOTHING
715             }
716             // PukiWikiではURL内の&のエスケープは不要?
717
718             wikiText.append("// ========== ");
719             wikiText.append(name)
720                     .append(" acts as [")
721                     .append(avatar.getName())
722                     .append("]");
723             wikiText.append(" ==========\n");
724
725             String teamColor =  "BGCOLOR("
726                               + WolfBBS.getTeamWikiColor(role)
727                               + "):";
728
729             String avatarIcon = iconSet.getAvatarIconWiki(avatar);
730
731             wikiText.append('|').append(teamColor);
732             wikiText.append(avatarIcon).append("&br;");
733
734             wikiText.append("[[").append(avatar.getName()).append("]]");
735
736             wikiText.append('|').append(teamColor);
737             wikiText.append("[[").append(WolfBBS.escapeWikiBracket(name));
738             if(urlText != null && urlText.length() > 0){
739                 wikiText.append('>').append(urlText);
740             }
741             wikiText.append("]]");
742
743             wikiText.append('|').append(teamColor);
744             wikiText.append(WolfBBS.getRoleIconWiki(role));
745             wikiText.append("&br;");
746             wikiText.append("[[");
747             wikiText.append(role.getRoleName());
748             wikiText.append("]]");
749
750             String destinyColor = WolfBBS.getDestinyColorWiki(destiny);
751             wikiText.append('|');
752             wikiText.append("BGCOLOR(").append(destinyColor).append("):");
753             if(destiny == Destiny.ALIVE){
754                 wikiText.append("最後まで&br;生存");
755             }else{
756                 wikiText.append(obitDay).append("日目").append("&br;");
757                 wikiText.append(destiny.getMessage());
758             }
759
760             wikiText.append('|');
761             wikiText.append(avatar.getJobTitle()).append('。');
762
763             if(avatar == Avatar.AVATAR_GERD){
764                 wikiText.append("寝てばかりいた。");
765             }else if(role == GameRole.HUNTER){
766                 CharSequence report = dumpHunterActivity();
767                 wikiText.append(report);
768             }else if(role == GameRole.SEER){
769                 CharSequence report = dumpSeerActivity();
770                 wikiText.append(report);
771             }
772
773             wikiText.append("|\n");
774
775         }
776
777         wikiText.append("|>|>|>|>|");
778         wikiText.append("RIGHT:");
779         wikiText.append("顔アイコン提供 : [[");
780         wikiText.append(WolfBBS.escapeWikiBracket(iconSet.getAuthor()));
781         wikiText.append(">")
782                 .append(iconSet.getUrlText());
783         wikiText.append("]]氏");
784         wikiText.append("|\n");
785
786         wikiText.append(WolfBBS.COMMENTLINE);
787         wikiText.append("// ↑キャスト表ここまで\n");
788         wikiText.append(WolfBBS.COMMENTLINE);
789
790         return wikiText;
791     }
792
793     /**
794      * 村詳細情報を出力する。
795      * @return 村詳細情報
796      */
797     public CharSequence dumpVillageWiki(){
798         StringBuilder wikiText = new StringBuilder();
799
800         DateFormat dform =
801                 DateFormat.getDateTimeInstance(DateFormat.FULL,
802                                                DateFormat.FULL);
803
804         String vName = this.village.getVillageFullName();
805
806         wikiText.append(WolfBBS.COMMENTLINE);
807         wikiText.append("// ↓村詳細開始\n");
808         wikiText.append("//        Village : ")
809                 .append(vName)
810                 .append('\n');
811         wikiText.append("//        Generator : ")
812                 .append(GENERATOR)
813                 .append('\n');
814
815         wikiText.append("* 村の詳細\n");
816
817         wikiText.append(WolfBBS.COMMENTLINE);
818         wikiText.append("- 勝者\n");
819         Team winnerTeam = getWinnerTeam();
820         String wonTeam = winnerTeam.getTeamName();
821         wikiText.append(wonTeam).append('\n');
822
823         wikiText.append(WolfBBS.COMMENTLINE);
824         wikiText.append("- エントリー開始時刻\n");
825         Date date = get1stTalkDate();
826         String talk1st = dform.format(date);
827         wikiText.append(talk1st).append('\n');
828
829         wikiText.append(WolfBBS.COMMENTLINE);
830         wikiText.append("- 参加人数\n");
831         int avatarNum = countAvatarNum();
832         String totalMember = "ゲルト + " + (avatarNum - 1) + "名 = "
833                             + avatarNum + "名";
834         wikiText.append(WolfBBS.escapeWikiSyntax(totalMember))
835                 .append('\n');
836
837         wikiText.append(WolfBBS.COMMENTLINE);
838         wikiText.append("- 役職内訳\n");
839         StringBuilder roleMsg = new StringBuilder();
840         for(GameRole role : GameRole.values()){
841             List<Player> players = getRoledPlayerList(role);
842             String roleName = role.getRoleName();
843             if(players.size() <= 0) continue;
844             if(roleMsg.length() > 0) roleMsg.append('、');
845             roleMsg.append(roleName)
846                    .append(" × ")
847                    .append(players.size())
848                    .append("名");
849         }
850         wikiText.append(WolfBBS.escapeWikiSyntax(roleMsg)).append('\n');
851
852         wikiText.append(WolfBBS.COMMENTLINE);
853         wikiText.append("- 処刑内訳\n");
854         wikiText.append(dumpExecutionInfo()).append('\n');
855
856         wikiText.append(WolfBBS.COMMENTLINE);
857         wikiText.append("- 襲撃内訳\n");
858         wikiText.append(dumpAssaultInfo()).append('\n');
859
860         wikiText.append(WolfBBS.COMMENTLINE);
861         wikiText.append("- 突然死\n");
862         wikiText.append(countSuddenDeath()).append("名").append('\n');
863
864         wikiText.append(WolfBBS.COMMENTLINE);
865         wikiText.append("- 人口推移\n");
866         for(int day = 1; day < this.village.getPeriodSize(); day++){
867             List<Player> players = getSurvivorList(day);
868             CharSequence roleSeq =
869                     GameSummary.getRoleBalanceSequence(players);
870             String daySeq;
871             Period period = this.village.getPeriod(day);
872             daySeq = period.getCaption();
873             wikiText.append('|')
874                     .append(daySeq)
875                     .append('|')
876                     .append(roleSeq)
877                     .append("|\n");
878         }
879
880         wikiText.append(WolfBBS.COMMENTLINE);
881         wikiText.append("- 占い師の成績\n");
882         wikiText.append(dumpSeerActivity()).append('\n');
883
884         wikiText.append(WolfBBS.COMMENTLINE);
885         wikiText.append("- 狩人の成績\n");
886         wikiText.append(dumpHunterActivity()).append('\n');
887
888         wikiText.append(WolfBBS.COMMENTLINE);
889         wikiText.append("// ↑村詳細ここまで\n");
890         wikiText.append(WolfBBS.COMMENTLINE);
891
892         return wikiText;
893     }
894
895     /**
896      * 最初の発言の時刻を得る。
897      * @return 時刻
898      */
899     public Date get1stTalkDate(){
900         return new Date(this.talk1stTimeMs);
901     }
902
903     /**
904      * 最後の発言の時刻を得る。
905      * @return 時刻
906      */
907     public Date getLastTalkDate(){
908         return new Date(this.talkLastTimeMs);
909     }
910
911     /**
912      * 指定した日の生存者一覧を得る。
913      * @param day 日
914      * @return 生存者一覧
915      */
916     public List<Player> getSurvivorList(int day){
917         if(day < 0 || this.village.getPeriodSize() <= day){
918             throw new IndexOutOfBoundsException();
919         }
920
921         List<Player> result = new LinkedList<Player>();
922
923         Period period = this.village.getPeriod(day);
924
925         if(   period.isPrologue()
926            || (period.isProgress() && day == 1) ){
927             result.addAll(this.playerList);
928             return result;
929         }
930
931         if(period.isEpilogue()){
932             for(Player player : this.playerList){
933                 if(player.getDestiny() == Destiny.ALIVE){
934                     result.add(player);
935                 }
936             }
937             return result;
938         }
939
940         for(Topic topic : period.getTopicList()){
941             if( ! (topic instanceof SysEvent) ) continue;
942             SysEvent sysEvent = (SysEvent) topic;
943             if(sysEvent.getSysEventType() == SysEventType.SURVIVOR){
944                 List<Avatar> avatarList = sysEvent.getAvatarList();
945                 for(Avatar avatar : avatarList){
946                     Player player = getPlayer(avatar);
947                     result.add(player);
948                 }
949             }
950         }
951
952         return result;
953     }
954
955     /**
956      * プレイヤー一覧を得る。
957      * 参加エントリー順
958      * @return プレイヤーのリスト
959      */
960     public List<Player> getPlayerList(){
961         List<Player> result = Collections.unmodifiableList(this.playerList);
962         return result;
963     }
964
965     /**
966      * キャスティング表用にソートされたプレイヤー一覧を得る。
967      * @return プレイヤーのリスト
968      */
969     public List<Player> getCastingPlayerList(){
970         List<Player> sortedPlayers =
971                 new LinkedList<Player>();
972         sortedPlayers.addAll(this.playerList);
973         Collections.sort(sortedPlayers, COMPARATOR_CASTING);
974         return sortedPlayers;
975     }
976
977     /**
978      * 指定された役職のプレイヤー一覧を得る。
979      * @param role 役職
980      * @return 役職に合致するプレイヤーのリスト
981      */
982     public List<Player> getRoledPlayerList(GameRole role){
983         List<Player> result = new LinkedList<Player>();
984
985         for(Player player : this.playerList){
986             if(player.getRole() == role){
987                 result.add(player);
988             }
989         }
990
991         return result;
992     }
993
994     /**
995      * 勝利陣営を得る。
996      * @return 勝利した陣営
997      */
998     public Team getWinnerTeam(){
999         return this.winner;
1000     }
1001
1002     /**
1003      * 突然死者数を得る。
1004      * @return 突然死者数
1005      */
1006     public int countSuddenDeath(){
1007         int suddenDeath = 0;
1008         for(Player player : this.playerList){
1009             if(player.getDestiny() == Destiny.SUDDENDEATH) suddenDeath++;
1010         }
1011         return suddenDeath;
1012     }
1013
1014     /**
1015      * 参加プレイヤー総数を得る。
1016      * @return プレイヤー総数
1017      */
1018     public int countAvatarNum(){
1019         int playerNum = this.playerList.size();
1020         return playerNum;
1021     }
1022
1023     /**
1024      * AvatarからPlayerを得る。
1025      * 参加していないAvatarならnullを返す。
1026      * @param avatar Avatar
1027      * @return Player
1028      */
1029     public final Player getPlayer(Avatar avatar){
1030         Player player = this.playerMap.get(avatar);
1031         return player;
1032     }
1033
1034     /**
1035      * AvatarからPlayerを得る。
1036      * 無ければ新規に作る。
1037      * @param avatar Avatar
1038      * @return Player
1039      */
1040     private Player registPlayer(Avatar avatar){
1041         Player player = getPlayer(avatar);
1042         if(player == null){
1043             player = new Player();
1044             player.setAvatar(avatar);
1045             this.playerMap.put(avatar, player);
1046         }
1047         return player;
1048     }
1049
1050     /**
1051      * プレイヤーのソート仕様の記述。
1052      * まとめサイトのキャスト表向け。
1053      */
1054     private static final class CastingComparator
1055             implements Comparator<Player> {
1056
1057         /**
1058          * コンストラクタ。
1059          */
1060         private CastingComparator(){
1061             super();
1062             return;
1063         }
1064
1065         /**
1066          * {@inheritDoc}
1067          * @param p1 {@inheritDoc}
1068          * @param p2 {@inheritDoc}
1069          * @return {@inheritDoc}
1070          */
1071         @Override
1072         public int compare(Player p1, Player p2){
1073             if(p1 == p2) return 0;
1074             if(p1 == null) return -1;
1075             if(p2 == null) return +1;
1076
1077             // ゲルトが最前
1078             Avatar avatar1 = p1.getAvatar();
1079             Avatar avatar2 = p2.getAvatar();
1080             if(avatar1.equals(avatar2)) return 0;
1081             if(avatar1 == Avatar.AVATAR_GERD) return -1;
1082             if(avatar2 == Avatar.AVATAR_GERD) return +1;
1083
1084             // 生存者は最後
1085             Destiny dest1 = p1.getDestiny();
1086             Destiny dest2 = p2.getDestiny();
1087             if(dest1 != dest2){
1088                 if     (dest1 == Destiny.ALIVE) return +1;
1089                 else if(dest2 == Destiny.ALIVE) return -1;
1090             }
1091
1092             // 退場順
1093             int obitDay1 = p1.getObitDay();
1094             int obitDay2 = p2.getObitDay();
1095             if(obitDay1 > obitDay2) return +1;
1096             if(obitDay1 < obitDay2) return -1;
1097
1098             // 運命順
1099             int destinyOrder = dest1.compareTo(dest2);
1100             if(destinyOrder != 0) return destinyOrder;
1101
1102             // エントリー順
1103             int entryOrder = p1.getEntryNo() - p2.getEntryNo();
1104
1105             return entryOrder;
1106         }
1107     }
1108
1109     // TODO E国ハムスター対応
1110 }