OSDN Git Service

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