-/*\r
- * Summarize game information\r
- *\r
- * Copyright(c) 2009 olyutorskii\r
- * $Id: GameSummary.java 1028 2010-05-13 10:15:11Z olyutorskii $\r
- */\r
-\r
-package jp.sourceforge.jindolf;\r
-\r
-import java.net.MalformedURLException;\r
-import java.net.URI;\r
-import java.net.URISyntaxException;\r
-import java.net.URL;\r
-import java.text.DateFormat;\r
-import java.util.Collections;\r
-import java.util.Comparator;\r
-import java.util.Date;\r
-import java.util.EnumMap;\r
-import java.util.HashMap;\r
-import java.util.LinkedList;\r
-import java.util.List;\r
-import java.util.Map;\r
-import java.util.Set;\r
-import jp.sourceforge.jindolf.corelib.Destiny;\r
-import jp.sourceforge.jindolf.corelib.GameRole;\r
-import jp.sourceforge.jindolf.corelib.SysEventType;\r
-import jp.sourceforge.jindolf.corelib.Team;\r
-import jp.sourceforge.jindolf.corelib.VillageState;\r
-\r
-/**\r
- * 決着の付いたゲームのサマリを集計。\r
- */\r
-public class GameSummary{\r
-\r
- /** キャスティング表示用Comparator。 */\r
- public static final Comparator<Player> COMPARATOR_CASTING =\r
- new CastingComparator();\r
-\r
- /**\r
- * プレイヤーのリストから役職バランス文字列を得る。\r
- * ex) "村村占霊狂狼"\r
- * @param players プレイヤーのリスト\r
- * @return 役職バランス文字列\r
- */\r
- public static String getRoleBalanceSequence(List<Player> players){\r
- List<GameRole> roleList = new LinkedList<GameRole>();\r
- for(Player player : players){\r
- GameRole role = player.getRole();\r
- roleList.add(role);\r
- }\r
- Collections.sort(roleList, GameRole.getPowerBalanceComparator());\r
-\r
- StringBuilder result = new StringBuilder();\r
- for(GameRole role : roleList){\r
- char ch = role.getShortName();\r
- result.append(ch);\r
- }\r
-\r
- return result.toString();\r
- }\r
-\r
- private final Map<Avatar, Player> playerMap =\r
- new HashMap<Avatar, Player>();\r
- private final List<Player> playerList =\r
- new LinkedList<Player>();\r
- private final Map<SysEventType, List<SysEvent>> eventMap =\r
- new EnumMap<SysEventType, List<SysEvent>>(SysEventType.class);\r
-\r
- private final Village village;\r
-\r
- // 勝者\r
- private Team winner;\r
-\r
- // 占い先集計\r
- private int ctScryVillage = 0;\r
- private int ctScryHamster = 0;\r
- private int ctScryMadman = 0;\r
- private int ctScryWolf = 0;\r
-\r
- // 護衛先集計\r
- private int ctGuardVillage = 0;\r
- private int ctGuardHamster = 0;\r
- private int ctGuardMadman = 0;\r
- private int ctGuardWolf = 0;\r
- private int ctGuardVillageGJ = 0;\r
- private int ctGuardHamsterGJ = 0;\r
- private int ctGuardMadmanGJ = 0;\r
- private int ctGuardFakeGJ = 0;\r
-\r
- // 発言時刻範囲\r
- private long talk1stTimeMs = -1;\r
- private long talkLastTimeMs = -1;\r
-\r
- /**\r
- * コンストラクタ。\r
- * @param village 村\r
- */\r
- public GameSummary(Village village){\r
- super();\r
-\r
- VillageState state = village.getState();\r
- if( state != VillageState.EPILOGUE\r
- && state != VillageState.GAMEOVER){\r
- throw new IllegalStateException();\r
- }\r
-\r
- this.village = village;\r
-\r
- summarize();\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * サマライズ処理。\r
- */\r
- private void summarize(){\r
- buildEventMap();\r
-\r
- summarizeTime();\r
- summarizeWinner();\r
- summarizePlayers();\r
-\r
- for(Period period : this.village.getPeriodList()){\r
- summarizePeriod(period);\r
- }\r
-\r
- summarizeJudge();\r
- summarizeGuard();\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * SysEventの種別ごとに集計する。\r
- */\r
- private void buildEventMap(){\r
- for(SysEventType type : SysEventType.values()){\r
- List<SysEvent> eventList = new LinkedList<SysEvent>();\r
- this.eventMap.put(type, eventList);\r
- }\r
-\r
- for(Period period : this.village.getPeriodList()){\r
- for(Topic topic : period.getTopicList()){\r
- if( ! (topic instanceof SysEvent) ) continue;\r
- SysEvent event = (SysEvent) topic;\r
- SysEventType type = event.getSysEventType();\r
- List<SysEvent> eventList = this.eventMap.get(type);\r
- eventList.add(event);\r
- }\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 勝者集計。\r
- */\r
- private void summarizeWinner(){\r
- List<SysEvent> eventList;\r
-\r
- eventList = this.eventMap.get(SysEventType.WINVILLAGE);\r
- if( ! eventList.isEmpty() ){\r
- this.winner = Team.VILLAGE;\r
- }\r
-\r
- eventList = this.eventMap.get(SysEventType.WINWOLF);\r
- if( ! eventList.isEmpty() ){\r
- this.winner = Team.WOLF;\r
- }\r
-\r
- eventList = this.eventMap.get(SysEventType.WINHAMSTER);\r
- if( ! eventList.isEmpty() ){\r
- this.winner = Team.HAMSTER;\r
- }\r
-\r
- if(this.winner == null) assert false;\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 参加者集計。\r
- */\r
- private void summarizePlayers(){\r
- List<SysEvent> eventList;\r
-\r
- List<Avatar> avatarList;\r
- List<GameRole> roleList;\r
- List<Integer> integerList;\r
- List<CharSequence> textList;\r
-\r
- eventList = this.eventMap.get(SysEventType.ONSTAGE);\r
- for(SysEvent event : eventList){\r
- avatarList = event.getAvatarList();\r
- integerList = event.getIntegerList();\r
- Avatar onstageAvatar = avatarList.get(0);\r
- Player onstagePlayer = registPlayer(onstageAvatar);\r
- onstagePlayer.setEntryNo(integerList.get(0));\r
- }\r
-\r
- eventList = this.eventMap.get(SysEventType.PLAYERLIST);\r
- assert eventList.size() == 1;\r
- SysEvent event = eventList.get(0);\r
-\r
- avatarList = event.getAvatarList();\r
- roleList = event.getRoleList();\r
- integerList = event.getIntegerList();\r
- textList = event.getCharSequenceList();\r
- int avatarNum = avatarList.size();\r
- for(int idx = 0; idx < avatarNum; idx++){\r
- Avatar avatar = avatarList.get(idx);\r
- GameRole role = roleList.get(idx);\r
- CharSequence urlText = textList.get(idx * 2);\r
- CharSequence idName = textList.get(idx * 2 + 1);\r
- int liveOrDead = integerList.get(idx);\r
-\r
- Player player = registPlayer(avatar);\r
- player.setRole(role);\r
- player.setUrlText(urlText.toString());\r
- player.setIdName(idName.toString());\r
- if(liveOrDead != 0){ // 生存\r
- player.setObitDay(-1);\r
- player.setDestiny(Destiny.ALIVE);\r
- }\r
-\r
- this.playerList.add(player);\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * Periodのサマライズ。\r
- * @param period Period\r
- */\r
- private void summarizePeriod(Period period){\r
- int day = period.getDay();\r
- for(Topic topic : period.getTopicList()){\r
- if(topic instanceof SysEvent){\r
- SysEvent sysEvent = (SysEvent) topic;\r
- summarizeDestiny(day, sysEvent);\r
- }\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 各プレイヤー運命のサマライズ。\r
- * @param day 日\r
- * @param sysEvent システムイベント\r
- */\r
- private void summarizeDestiny(int day, SysEvent sysEvent){\r
- List<Avatar> avatarList = sysEvent.getAvatarList();\r
- List<Integer> integerList = sysEvent.getIntegerList();\r
-\r
- int avatarTotal = avatarList.size();\r
- Avatar lastAvatar = null;\r
- if(avatarTotal > 0) lastAvatar = avatarList.get(avatarTotal - 1);\r
-\r
- SysEventType eventType = sysEvent.getSysEventType();\r
- switch(eventType){\r
- case EXECUTION: // G国のみ\r
- if(integerList.get(avatarTotal - 1) > 0) break; // 処刑無し\r
- Player executedPl = registPlayer(lastAvatar);\r
- executedPl.setDestiny(Destiny.EXECUTED);\r
- executedPl.setObitDay(day);\r
- break;\r
- case SUDDENDEATH:\r
- Avatar suddenDeathAvatar = avatarList.get(0);\r
- Player suddenDeathPlayer = registPlayer(suddenDeathAvatar);\r
- suddenDeathPlayer.setDestiny(Destiny.SUDDENDEATH);\r
- suddenDeathPlayer.setObitDay(day);\r
- break;\r
- case COUNTING: // G国COUNTING2は運命に関係なし\r
- if(avatarTotal % 2 == 0) break; // 処刑無し\r
- Player executedPlayer = registPlayer(lastAvatar);\r
- executedPlayer.setDestiny(Destiny.EXECUTED);\r
- executedPlayer.setObitDay(day);\r
- break;\r
- case MURDERED:\r
- for(Avatar avatar : avatarList){\r
- Player player = registPlayer(avatar);\r
- player.setDestiny(Destiny.EATEN);\r
- player.setObitDay(day);\r
- }\r
- // TODO E国ハム溶け処理は後回し\r
- break;\r
- default:\r
- break;\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 会話時刻のサマライズ。\r
- */\r
- private void summarizeTime(){\r
- for(Period period : this.village.getPeriodList()){\r
- for(Topic topic : period.getTopicList()){\r
- if( ! (topic instanceof Talk) ) continue;\r
- Talk talk = (Talk) topic;\r
-\r
- long epoch = talk.getTimeFromID();\r
-\r
- if(this.talk1stTimeMs < 0) this.talk1stTimeMs = epoch;\r
- if(this.talkLastTimeMs < 0) this.talkLastTimeMs = epoch;\r
-\r
- if(epoch < this.talk1stTimeMs ) this.talk1stTimeMs = epoch;\r
- if(epoch > this.talkLastTimeMs) this.talkLastTimeMs = epoch;\r
- }\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 占い師の活動を集計する。\r
- */\r
- private void summarizeJudge(){\r
- List<SysEvent> eventList = this.eventMap.get(SysEventType.JUDGE);\r
-\r
- for(SysEvent event : eventList){\r
- List<Avatar> avatarList = event.getAvatarList();\r
- Avatar avatar = avatarList.get(1);\r
- Player seered = getPlayer(avatar);\r
- GameRole role = seered.getRole();\r
- switch(role){\r
- case WOLF: this.ctScryWolf++; break;\r
- case MADMAN: this.ctScryMadman++; break;\r
- case HAMSTER: this.ctScryHamster++; break;\r
- default: this.ctScryVillage++; break;\r
- }\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 占い師の活動を文字列化する。\r
- * @return 占い師の活動\r
- */\r
- public CharSequence dumpSeerActivity(){\r
- StringBuilder result = new StringBuilder();\r
-\r
- if(this.ctScryVillage > 0){\r
- result.append("村陣営を");\r
- result.append(this.ctScryVillage);\r
- result.append("回");\r
- }\r
-\r
- if(this.ctScryHamster > 0){\r
- if(result.length() > 0) result.append('、');\r
- result.append("ハムスターを");\r
- result.append(this.ctScryHamster);\r
- result.append("回");\r
- }\r
-\r
- if(this.ctScryMadman > 0){\r
- if(result.length() > 0) result.append('、');\r
- result.append("狂人を");\r
- result.append(this.ctScryMadman);\r
- result.append("回");\r
- }\r
-\r
- if(this.ctScryWolf > 0){\r
- if(result.length() > 0) result.append('、');\r
- result.append("人狼を");\r
- result.append(this.ctScryWolf);\r
- result.append("回");\r
- }\r
-\r
- if(result.length() <= 0) result.append("誰も占わなかった。");\r
- else result.append("占った。");\r
-\r
- CharSequence seq = WolfBBS.escapeWikiSyntax(result);\r
-\r
- return seq;\r
- }\r
-\r
- /**\r
- * 狩人の活動を集計する。\r
- */\r
- private void summarizeGuard(){\r
- List<SysEvent> eventList;\r
-\r
- eventList = this.eventMap.get(SysEventType.GUARD);\r
- for(SysEvent event : eventList){\r
- List<Avatar> avatarList = event.getAvatarList();\r
- Avatar avatar = avatarList.get(1);\r
- Player guarded = getPlayer(avatar);\r
- GameRole guardedRole = guarded.getRole();\r
- switch(guardedRole){\r
- case WOLF: this.ctGuardWolf++; break;\r
- case MADMAN: this.ctGuardMadman++; break;\r
- case HAMSTER: this.ctGuardHamster++; break;\r
- default: this.ctGuardVillage++; break;\r
- }\r
- }\r
-\r
- for(Period period : this.village.getPeriodList()){\r
- summarizeGjPeriod(period);\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 狩人GJの日ごとの集計。\r
- * @param period 日\r
- */\r
- private void summarizeGjPeriod(Period period){\r
- if(period.getDay() <= 2) return;\r
-\r
- boolean hasAssaultTried = period.hasAssaultTried();\r
- boolean hunterAlive = false;\r
- int wolfNum = 0;\r
-\r
- Set<Avatar> voters = period.getVoterSet();\r
- for(Avatar avatar : voters){\r
- Player player = getPlayer(avatar);\r
- switch(player.getRole()){\r
- case HUNTER: hunterAlive = true; break;\r
- case WOLF: wolfNum++; break;\r
- default: break;\r
- }\r
- }\r
-\r
- Avatar executed = period.getExecutedAvatar();\r
- if(executed != null){\r
- Player player = getPlayer(executed);\r
- switch(player.getRole()){\r
- case HUNTER: hunterAlive = false; break;\r
- case WOLF: wolfNum--; break;\r
- default: break;\r
- }\r
- }\r
-\r
- if( ! hunterAlive || wolfNum <= 0) return;\r
-\r
- SysEvent sysEvent;\r
-\r
- sysEvent = period.getTypedSysEvent(SysEventType.NOMURDER);\r
- if(sysEvent == null) return;\r
-\r
- sysEvent = period.getTypedSysEvent(SysEventType.GUARD);\r
- if(sysEvent == null) return;\r
-\r
- if(hasAssaultTried){\r
- Avatar guarded = sysEvent.getAvatarList().get(1);\r
- Player guardedPlayer = getPlayer(guarded);\r
- GameRole guardedRole = guardedPlayer.getRole();\r
- switch(guardedRole){\r
- case MADMAN: this.ctGuardMadmanGJ++; break;\r
- case HAMSTER: this.ctGuardHamsterGJ++; break;\r
- default: this.ctGuardVillageGJ++; break;\r
- }\r
- }else{\r
- this.ctGuardFakeGJ++; // 偽装GJ\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 狩人の活動を文字列化する。\r
- * @return 狩人の活動\r
- */\r
- public CharSequence dumpHunterActivity(){\r
- StringBuilder result = new StringBuilder();\r
-\r
- String atLeast;\r
- if(this.ctGuardFakeGJ > 0) atLeast = "少なくとも";\r
- else atLeast = "";\r
-\r
- if(this.ctGuardVillage > 0){\r
- result.append(atLeast);\r
- result.append("村陣営を");\r
- result.append(this.ctGuardVillage);\r
- result.append("回護衛し");\r
- if(this.ctGuardVillageGJ > 0){\r
- result.append("GJを");\r
- result.append(this.ctGuardVillageGJ);\r
- result.append("回出した。");\r
- }else{\r
- result.append("た。");\r
- }\r
- }\r
-\r
- if(this.ctGuardHamster > 0){\r
- result.append(atLeast);\r
- result.append("ハムスターを");\r
- result.append(this.ctGuardHamster);\r
- result.append("回護衛し");\r
- if(this.ctGuardHamsterGJ > 0){\r
- result.append("GJを");\r
- result.append(this.ctGuardHamsterGJ);\r
- result.append("回出した。");\r
- }else{\r
- result.append("た。");\r
- }\r
- }\r
-\r
- if(this.ctGuardMadman > 0){\r
- result.append(atLeast);\r
- result.append("狂人を");\r
- result.append(this.ctGuardMadman);\r
- result.append("回護衛し");\r
- if(this.ctGuardMadmanGJ > 0){\r
- result.append("GJを");\r
- result.append(this.ctGuardMadmanGJ);\r
- result.append("回出した。");\r
- }else{\r
- result.append("た。");\r
- }\r
- }\r
-\r
- if(this.ctGuardWolf > 0){\r
- result.append(atLeast);\r
- result.append("人狼を");\r
- result.append(this.ctGuardWolf);\r
- result.append("回護衛した。");\r
- }\r
-\r
- if(this.ctGuardFakeGJ > 0){\r
- result.append("護衛先は不明ながら偽装GJが");\r
- result.append(this.ctGuardFakeGJ);\r
- result.append("回あった。");\r
- }\r
-\r
- if(result.length() <= 0) result.append("誰も護衛できなかった");\r
-\r
- CharSequence seq = WolfBBS.escapeWikiSyntax(result);\r
-\r
- return seq;\r
- }\r
-\r
- /**\r
- * 処刑概観を文字列化する。\r
- * @return 文字列化した処刑概観\r
- */\r
- public CharSequence dumpExecutionInfo(){\r
- StringBuilder result = new StringBuilder();\r
-\r
- int exeWolf = 0;\r
- int exeMad = 0;\r
- int exeVillage = 0;\r
- for(Player player : this.playerList){\r
- Destiny destiny = player.getDestiny();\r
- if(destiny != Destiny.EXECUTED) continue;\r
- GameRole role = player.getRole();\r
- switch(role){\r
- case WOLF: exeWolf++; break;\r
- case MADMAN: exeMad++; break;\r
- default: exeVillage++; break;\r
- }\r
- }\r
-\r
- if(exeVillage > 0){\r
- result.append("▼村陣営×").append(exeVillage).append("回");\r
- }\r
- if(exeMad > 0){\r
- if(result.length() > 0) result.append("、");\r
- result.append("▼狂×").append(exeMad).append("回");\r
- }\r
- if(exeWolf > 0){\r
- if(result.length() > 0) result.append("、");\r
- result.append("▼狼×").append(exeWolf).append("回");\r
- }\r
- if(result.length() <= 0) result.append("なし");\r
-\r
- CharSequence seq = WolfBBS.escapeWikiSyntax(result);\r
-\r
- return seq;\r
- }\r
-\r
- /**\r
- * 襲撃概観を文字列化する。\r
- * @return 文字列化した襲撃概観\r
- */\r
- public CharSequence dumpAssaultInfo(){\r
- StringBuilder result = new StringBuilder();\r
-\r
- int eatMad = 0;\r
- int eatVillage = 0;\r
- for(Player player : this.playerList){\r
- if(player.getAvatar() == Avatar.AVATAR_GERD){\r
- result.append("▲ゲルト");\r
- continue;\r
- }\r
- Destiny destiny = player.getDestiny();\r
- if(destiny != Destiny.EATEN) continue;\r
- GameRole role = player.getRole();\r
- switch(role){\r
- case MADMAN: eatMad++; break;\r
- default: eatVillage++; break;\r
- }\r
- }\r
-\r
- if(eatVillage > 0){\r
- if(result.length() > 0) result.append("、");\r
- result.append("▲村陣営×").append(eatVillage).append("回");\r
- }\r
- if(eatMad > 0){\r
- if(result.length() > 0) result.append("、");\r
- result.append("▲狂×").append(eatMad).append("回");\r
- }\r
-\r
- if(result.length() <= 0) result.append("襲撃なし");\r
-\r
- CharSequence seq = WolfBBS.escapeWikiSyntax(result);\r
-\r
- return seq;\r
- }\r
-\r
- /**\r
- * まとめサイト用投票Boxを生成する。\r
- * @return 投票BoxのWikiテキスト\r
- */\r
- public CharSequence dumpVoteBox(){\r
- StringBuilder wikiText = new StringBuilder();\r
-\r
- for(Player player : getCastingPlayerList()){\r
- Avatar avatar = player.getAvatar();\r
- if(avatar == Avatar.AVATAR_GERD) continue;\r
- GameRole role = player.getRole();\r
- CharSequence fullName = avatar.getFullName();\r
- CharSequence roleName = role.getRoleName();\r
- StringBuilder line = new StringBuilder();\r
- line.append("[").append(roleName).append("] ").append(fullName);\r
- if(wikiText.length() > 0) wikiText.append(',');\r
- wikiText.append(WolfBBS.escapeWikiSyntax(line));\r
- wikiText.append("[0]");\r
- }\r
-\r
- wikiText.insert(0, "#vote(").append(")\n");\r
-\r
- return wikiText;\r
- }\r
-\r
- /**\r
- * まとめサイト用キャスト表を生成する。\r
- * @param iconSet 顔アイコンセット\r
- * @return キャスト表のWikiテキスト\r
- */\r
- public CharSequence dumpCastingBoard(FaceIconSet iconSet){\r
- StringBuilder wikiText = new StringBuilder();\r
-\r
- String vName = this.village.getVillageFullName();\r
- String generator = Jindolf.TITLE + " Ver." + Jindolf.VERSION;\r
- String author = iconSet.getAuthor() + "氏"\r
- +" [ "+iconSet.getUrlText()+" ]";\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("// ↓キャスト表開始\n");\r
- wikiText.append("// Village : " + vName + "\n");\r
- wikiText.append("// Generator : " + generator + "\n");\r
- wikiText.append("// アイコン作者 : " + author + '\n');\r
- wikiText.append("// ※アイコン画像の著作財産権保持者"\r
- +"および画像サーバ運営者から\n");\r
- wikiText.append("// 新しい意向が示された場合、"\r
- +"そちらを最優先で尊重してください。\n");\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
-\r
- wikiText.append("|配役")\r
- .append("|参加者")\r
- .append("|役職")\r
- .append("|運命")\r
- .append("|その活躍")\r
- .append("|h")\r
- .append('\n');\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
-\r
- for(Player player : getCastingPlayerList()){\r
- Avatar avatar = player.getAvatar();\r
- GameRole role = player.getRole();\r
- Destiny destiny = player.getDestiny();\r
- int obitDay = player.getObitDay();\r
- String name = player.getIdName();\r
- String urlText = player.getUrlText();\r
- if(urlText == null) urlText = "";\r
- urlText = urlText.replace("~", "%7e");\r
- urlText = urlText.replace(" ", "%20");\r
- try{\r
- URL url = new URL(urlText);\r
- URI uri = url.toURI();\r
- urlText = uri.toASCIIString();\r
- }catch(MalformedURLException e){\r
- // NOTHING\r
- }catch(URISyntaxException e){\r
- // NOTHING\r
- }\r
- // PukiWikiではURL内の&のエスケープは不要?\r
-\r
- wikiText.append("// ========== ");\r
- wikiText.append(name + " acts as [" + avatar.getName() + "]");\r
- wikiText.append(" ==========\n");\r
-\r
- String teamColor = "BGCOLOR("\r
- + WolfBBS.getTeamWikiColor(role)\r
- + "):";\r
-\r
- String avatarIcon = iconSet.getAvatarIconWiki(avatar);\r
-\r
- wikiText.append('|').append(teamColor);\r
- wikiText.append(avatarIcon).append("&br;");\r
-\r
- wikiText.append("[[").append(avatar.getName()).append("]]");\r
-\r
- wikiText.append('|').append(teamColor);\r
- wikiText.append("[[").append(WolfBBS.escapeWikiBracket(name));\r
- if(urlText != null && urlText.length() > 0){\r
- wikiText.append('>').append(urlText);\r
- }\r
- wikiText.append("]]");\r
-\r
- wikiText.append('|').append(teamColor);\r
- wikiText.append(WolfBBS.getRoleIconWiki(role));\r
- wikiText.append("&br;");\r
- wikiText.append("[[");\r
- wikiText.append(role.getRoleName());\r
- wikiText.append("]]");\r
-\r
- String destinyColor = WolfBBS.getDestinyColorWiki(destiny);\r
- wikiText.append('|');\r
- wikiText.append("BGCOLOR(").append(destinyColor).append("):");\r
- if(destiny == Destiny.ALIVE){\r
- wikiText.append("最後まで&br;生存");\r
- }else{\r
- wikiText.append(obitDay).append("日目").append("&br;");\r
- wikiText.append(destiny.getMessage());\r
- }\r
-\r
- wikiText.append('|');\r
- wikiText.append(avatar.getJobTitle()).append('。');\r
-\r
- if(avatar == Avatar.AVATAR_GERD){\r
- wikiText.append("寝てばかりいた。");\r
- }else if(role == GameRole.HUNTER){\r
- CharSequence report = dumpHunterActivity();\r
- wikiText.append(report);\r
- }else if(role == GameRole.SEER){\r
- CharSequence report = dumpSeerActivity();\r
- wikiText.append(report);\r
- }\r
-\r
- wikiText.append("|\n");\r
-\r
- }\r
-\r
- wikiText.append("|>|>|>|>|");\r
- wikiText.append("RIGHT:");\r
- wikiText.append("顔アイコン提供 : [[");\r
- wikiText.append(WolfBBS.escapeWikiBracket(iconSet.getAuthor()));\r
- wikiText.append(">" + iconSet.getUrlText());\r
- wikiText.append("]]氏");\r
- wikiText.append("|\n");\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("// ↑キャスト表ここまで\n");\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
-\r
- return wikiText;\r
- }\r
-\r
- /**\r
- * 村詳細情報を出力する。\r
- * @return 村詳細情報\r
- */\r
- public CharSequence dumpVillageWiki(){\r
- StringBuilder wikiText = new StringBuilder();\r
-\r
- DateFormat dform =\r
- DateFormat.getDateTimeInstance(DateFormat.FULL,\r
- DateFormat.FULL);\r
-\r
- String vName = this.village.getVillageFullName();\r
- String generator = Jindolf.TITLE + " Ver." + Jindolf.VERSION;\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("// ↓村詳細開始\n");\r
- wikiText.append("// Village : " + vName + "\n");\r
- wikiText.append("// Generator : " + generator + "\n");\r
-\r
- wikiText.append("* 村の詳細\n");\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 勝者\n");\r
- Team winnerTeam = getWinnerTeam();\r
- String wonTeam = winnerTeam.getTeamName();\r
- wikiText.append(wonTeam).append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- エントリー開始時刻\n");\r
- Date date = get1stTalkDate();\r
- String talk1st = dform.format(date);\r
- wikiText.append(talk1st).append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 参加人数\n");\r
- int avatarNum = countAvatarNum();\r
- String totalMember = "ゲルト + " + (avatarNum - 1) + "名 = "\r
- + avatarNum + "名";\r
- wikiText.append(WolfBBS.escapeWikiSyntax(totalMember))\r
- .append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 役職内訳\n");\r
- StringBuilder roleMsg = new StringBuilder();\r
- for(GameRole role : GameRole.values()){\r
- List<Player> players = getRoledPlayerList(role);\r
- String roleName = role.getRoleName();\r
- if(players.size() <= 0) continue;\r
- if(roleMsg.length() > 0) roleMsg.append('、');\r
- roleMsg.append(roleName)\r
- .append(" × ")\r
- .append(players.size())\r
- .append("名");\r
- }\r
- wikiText.append(WolfBBS.escapeWikiSyntax(roleMsg)).append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 処刑内訳\n");\r
- wikiText.append(dumpExecutionInfo()).append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 襲撃内訳\n");\r
- wikiText.append(dumpAssaultInfo()).append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 突然死\n");\r
- wikiText.append(countSuddenDeath()).append("名").append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 人口推移\n");\r
- for(int day = 1; day < this.village.getPeriodSize(); day++){\r
- List<Player> players = getSurvivorList(day);\r
- CharSequence roleSeq =\r
- GameSummary.getRoleBalanceSequence(players);\r
- String daySeq;\r
- Period period = this.village.getPeriod(day);\r
- daySeq = period.getCaption();\r
- wikiText.append('|')\r
- .append(daySeq)\r
- .append('|')\r
- .append(roleSeq)\r
- .append("|\n");\r
- }\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 占い師の成績\n");\r
- wikiText.append(dumpSeerActivity()).append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("- 狩人の成績\n");\r
- wikiText.append(dumpHunterActivity()).append('\n');\r
-\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
- wikiText.append("// ↑村詳細ここまで\n");\r
- wikiText.append(WolfBBS.COMMENTLINE);\r
-\r
- return wikiText;\r
- }\r
-\r
- /**\r
- * 最初の発言の時刻を得る。\r
- * @return 時刻\r
- */\r
- public Date get1stTalkDate(){\r
- return new Date(this.talk1stTimeMs);\r
- }\r
-\r
- /**\r
- * 最後の発言の時刻を得る。\r
- * @return 時刻\r
- */\r
- public Date getLastTalkDate(){\r
- return new Date(this.talkLastTimeMs);\r
- }\r
-\r
- /**\r
- * 指定した日の生存者一覧を得る。\r
- * @param day 日\r
- * @return 生存者一覧\r
- */\r
- public List<Player> getSurvivorList(int day){\r
- if(day < 0 || this.village.getPeriodSize() <= day){\r
- throw new IndexOutOfBoundsException();\r
- }\r
-\r
- List<Player> result = new LinkedList<Player>();\r
-\r
- Period period = this.village.getPeriod(day);\r
-\r
- if( period.isPrologue()\r
- || (period.isProgress() && day == 1) ){\r
- result.addAll(this.playerList);\r
- return result;\r
- }\r
-\r
- if(period.isEpilogue()){\r
- for(Player player : this.playerList){\r
- if(player.getDestiny() == Destiny.ALIVE){\r
- result.add(player);\r
- }\r
- }\r
- return result;\r
- }\r
-\r
- for(Topic topic : period.getTopicList()){\r
- if( ! (topic instanceof SysEvent) ) continue;\r
- SysEvent sysEvent = (SysEvent) topic;\r
- if(sysEvent.getSysEventType() == SysEventType.SURVIVOR){\r
- List<Avatar> avatarList = sysEvent.getAvatarList();\r
- for(Avatar avatar : avatarList){\r
- Player player = getPlayer(avatar);\r
- result.add(player);\r
- }\r
- }\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * プレイヤー一覧を得る。\r
- * 参加エントリー順\r
- * @return プレイヤーのリスト\r
- */\r
- public List<Player> getPlayerList(){\r
- List<Player> result = Collections.unmodifiableList(this.playerList);\r
- return result;\r
- }\r
-\r
- /**\r
- * キャスティング表用にソートされたプレイヤー一覧を得る。\r
- * @return プレイヤーのリスト\r
- */\r
- public List<Player> getCastingPlayerList(){\r
- List<Player> sortedPlayers =\r
- new LinkedList<Player>();\r
- sortedPlayers.addAll(this.playerList);\r
- Collections.sort(sortedPlayers, COMPARATOR_CASTING);\r
- return sortedPlayers;\r
- }\r
-\r
- /**\r
- * 指定された役職のプレイヤー一覧を得る。\r
- * @param role 役職\r
- * @return 役職に合致するプレイヤーのリスト\r
- */\r
- public List<Player> getRoledPlayerList(GameRole role){\r
- List<Player> result = new LinkedList<Player>();\r
-\r
- for(Player player : this.playerList){\r
- if(player.getRole() == role){\r
- result.add(player);\r
- }\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * 勝利陣営を得る。\r
- * @return 勝利した陣営\r
- */\r
- public Team getWinnerTeam(){\r
- return this.winner;\r
- }\r
-\r
- /**\r
- * 突然死者数を得る。\r
- * @return 突然死者数\r
- */\r
- public int countSuddenDeath(){\r
- int suddenDeath = 0;\r
- for(Player player : this.playerList){\r
- if(player.getDestiny() == Destiny.SUDDENDEATH) suddenDeath++;\r
- }\r
- return suddenDeath;\r
- }\r
-\r
- /**\r
- * 参加プレイヤー総数を得る。\r
- * @return プレイヤー総数\r
- */\r
- public int countAvatarNum(){\r
- int playerNum = this.playerList.size();\r
- return playerNum;\r
- }\r
-\r
- /**\r
- * AvatarからPlayerを得る。\r
- * 参加していないAvatarならnullを返す。\r
- * @param avatar Avatar\r
- * @return Player\r
- */\r
- public final Player getPlayer(Avatar avatar){\r
- Player player = this.playerMap.get(avatar);\r
- return player;\r
- }\r
-\r
- /**\r
- * AvatarからPlayerを得る。\r
- * 無ければ新規に作る。\r
- * @param avatar Avatar\r
- * @return Player\r
- */\r
- private Player registPlayer(Avatar avatar){\r
- Player player = getPlayer(avatar);\r
- if(player == null){\r
- player = new Player();\r
- player.setAvatar(avatar);\r
- this.playerMap.put(avatar, player);\r
- }\r
- return player;\r
- }\r
-\r
- /**\r
- * プレイヤーのソート仕様の記述。\r
- * まとめサイトのキャスト表向け。\r
- */\r
- private static final class CastingComparator\r
- implements Comparator<Player> {\r
-\r
- /**\r
- * コンストラクタ。\r
- */\r
- private CastingComparator(){\r
- super();\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param p1 {@inheritDoc}\r
- * @param p2 {@inheritDoc}\r
- * @return {@inheritDoc}\r
- */\r
- public int compare(Player p1, Player p2){\r
- if(p1 == p2) return 0;\r
- if(p1 == null) return -1;\r
- if(p2 == null) return +1;\r
-\r
- // ゲルトが最前\r
- Avatar avatar1 = p1.getAvatar();\r
- Avatar avatar2 = p2.getAvatar();\r
- if(avatar1.equals(avatar2)) return 0;\r
- if(avatar1 == Avatar.AVATAR_GERD) return -1;\r
- if(avatar2 == Avatar.AVATAR_GERD) return +1;\r
-\r
- // 生存者は最後\r
- Destiny dest1 = p1.getDestiny();\r
- Destiny dest2 = p2.getDestiny();\r
- if(dest1 != dest2){\r
- if (dest1 == Destiny.ALIVE) return +1;\r
- else if(dest2 == Destiny.ALIVE) return -1;\r
- }\r
-\r
- // 退場順\r
- int obitDay1 = p1.getObitDay();\r
- int obitDay2 = p2.getObitDay();\r
- if(obitDay1 > obitDay2) return +1;\r
- if(obitDay1 < obitDay2) return -1;\r
-\r
- // 運命順\r
- int destinyOrder = dest1.compareTo(dest2);\r
- if(destinyOrder != 0) return destinyOrder;\r
-\r
- // エントリー順\r
- int entryOrder = p1.getEntryNo() - p2.getEntryNo();\r
-\r
- return entryOrder;\r
- }\r
- }\r
-\r
- // TODO E国ハムスター対応\r
-}\r
+/*
+ * Summarize game information
+ *
+ * License : The MIT License
+ * Copyright(c) 2009 olyutorskii
+ */
+
+package jp.sfjp.jindolf.summary;
+
+import java.awt.Color;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.DateFormat;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import jp.sfjp.jindolf.VerInfo;
+import jp.sfjp.jindolf.data.Avatar;
+import jp.sfjp.jindolf.data.Period;
+import jp.sfjp.jindolf.data.Player;
+import jp.sfjp.jindolf.data.SysEvent;
+import jp.sfjp.jindolf.data.Talk;
+import jp.sfjp.jindolf.data.Topic;
+import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.dxchg.FaceIconSet;
+import jp.sfjp.jindolf.dxchg.WolfBBS;
+import jp.sourceforge.jindolf.corelib.Destiny;
+import jp.sourceforge.jindolf.corelib.GameRole;
+import jp.sourceforge.jindolf.corelib.SysEventType;
+import jp.sourceforge.jindolf.corelib.Team;
+import jp.sourceforge.jindolf.corelib.VillageState;
+
+/**
+ * 決着の付いたゲームのサマリを集計。
+ */
+public class GameSummary{
+
+ /** キャスティング表示用Comparator。 */
+ public static final Comparator<Player> COMPARATOR_CASTING =
+ new CastingComparator();
+
+ private static final Color COLOR_PLAINTABLE = new Color(0xedf5fe);
+
+ private static final String GENERATOR =
+ VerInfo.TITLE + "\u0020Ver." + VerInfo.VERSION;
+
+
+ private final Map<Avatar, Player> playerMap =
+ new HashMap<>();
+ private final List<Player> playerList =
+ new LinkedList<>();
+ private final Map<SysEventType, List<SysEvent>> eventMap =
+ new EnumMap<>(SysEventType.class);
+
+ private final Village village;
+
+ // 勝者
+ private Team winner;
+
+ // 占い先集計
+ private int ctScryVillage = 0;
+ private int ctScryHamster = 0;
+ private int ctScryMadman = 0;
+ private int ctScryWolf = 0;
+
+ // 護衛先集計
+ private int ctGuardVillage = 0;
+ private int ctGuardHamster = 0;
+ private int ctGuardMadman = 0;
+ private int ctGuardWolf = 0;
+ private int ctGuardVillageGJ = 0;
+ private int ctGuardHamsterGJ = 0;
+ private int ctGuardMadmanGJ = 0;
+ private int ctGuardFakeGJ = 0;
+
+ // 発言時刻範囲
+ private long talk1stTimeMs = -1;
+ private long talkLastTimeMs = -1;
+
+
+ /**
+ * コンストラクタ。
+ * @param village 村
+ */
+ public GameSummary(Village village){
+ super();
+
+ VillageState state = village.getState();
+ if( state != VillageState.EPILOGUE
+ && state != VillageState.GAMEOVER){
+ throw new IllegalStateException();
+ }
+
+ this.village = village;
+
+ summarize();
+
+ return;
+ }
+
+
+ /**
+ * プレイヤーのリストから役職バランス文字列を得る。
+ * ex) "村村占霊狂狼"
+ * @param players プレイヤーのリスト
+ * @return 役職バランス文字列
+ */
+ public static String getRoleBalanceSequence(List<Player> players){
+ List<GameRole> roleList = new LinkedList<>();
+ for(Player player : players){
+ GameRole role = player.getRole();
+ roleList.add(role);
+ }
+ Collections.sort(roleList, GameRole.getPowerBalanceComparator());
+
+ StringBuilder result = new StringBuilder();
+ for(GameRole role : roleList){
+ char ch = role.getShortName();
+ result.append(ch);
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * サマライズ処理。
+ */
+ private void summarize(){
+ buildEventMap();
+
+ summarizeTime();
+ summarizeWinner();
+ summarizePlayers();
+
+ for(Period period : this.village.getPeriodList()){
+ summarizePeriod(period);
+ }
+
+ summarizeJudge();
+ summarizeGuard();
+
+ return;
+ }
+
+ /**
+ * SysEventの種別ごとに集計する。
+ */
+ private void buildEventMap(){
+ for(SysEventType type : SysEventType.values()){
+ List<SysEvent> eventList = new LinkedList<>();
+ this.eventMap.put(type, eventList);
+ }
+
+ for(Period period : this.village.getPeriodList()){
+ for(Topic topic : period.getTopicList()){
+ if( ! (topic instanceof SysEvent) ) continue;
+ SysEvent event = (SysEvent) topic;
+ SysEventType type = event.getSysEventType();
+ List<SysEvent> eventList = this.eventMap.get(type);
+ eventList.add(event);
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * 勝者集計。
+ */
+ private void summarizeWinner(){
+ List<SysEvent> eventList;
+
+ eventList = this.eventMap.get(SysEventType.WINVILLAGE);
+ if( ! eventList.isEmpty() ){
+ this.winner = Team.VILLAGE;
+ }
+
+ eventList = this.eventMap.get(SysEventType.WINWOLF);
+ if( ! eventList.isEmpty() ){
+ this.winner = Team.WOLF;
+ }
+
+ eventList = this.eventMap.get(SysEventType.WINHAMSTER);
+ if( ! eventList.isEmpty() ){
+ this.winner = Team.HAMSTER;
+ }
+
+ if(this.winner == null) assert false;
+
+ return;
+ }
+
+ /**
+ * 参加者集計。
+ */
+ private void summarizePlayers(){
+ List<SysEvent> eventList;
+
+ List<Avatar> avatarList;
+ List<GameRole> roleList;
+ List<Integer> integerList;
+ List<CharSequence> textList;
+
+ eventList = this.eventMap.get(SysEventType.ONSTAGE);
+ for(SysEvent event : eventList){
+ avatarList = event.getAvatarList();
+ integerList = event.getIntegerList();
+ Avatar onstageAvatar = avatarList.get(0);
+ Player onstagePlayer = registPlayer(onstageAvatar);
+ onstagePlayer.setEntryNo(integerList.get(0));
+ }
+
+ eventList = this.eventMap.get(SysEventType.PLAYERLIST);
+ assert eventList.size() == 1;
+ SysEvent event = eventList.get(0);
+
+ avatarList = event.getAvatarList();
+ roleList = event.getRoleList();
+ integerList = event.getIntegerList();
+ textList = event.getCharSequenceList();
+ int avatarNum = avatarList.size();
+ for(int idx = 0; idx < avatarNum; idx++){
+ Avatar avatar = avatarList.get(idx);
+ GameRole role = roleList.get(idx);
+ CharSequence urlText = textList.get(idx * 2);
+ CharSequence idName = textList.get(idx * 2 + 1);
+ int liveOrDead = integerList.get(idx);
+
+ Player player = registPlayer(avatar);
+ player.setRole(role);
+ player.setUrlText(urlText.toString());
+ player.setIdName(idName.toString());
+ if(liveOrDead != 0){ // 生存
+ player.setObitDay(-1);
+ player.setDestiny(Destiny.ALIVE);
+ }
+
+ this.playerList.add(player);
+ }
+
+ return;
+ }
+
+ /**
+ * Periodのサマライズ。
+ * @param period Period
+ */
+ private void summarizePeriod(Period period){
+ int day = period.getDay();
+ for(Topic topic : period.getTopicList()){
+ if(topic instanceof SysEvent){
+ SysEvent sysEvent = (SysEvent) topic;
+ summarizeDestiny(day, sysEvent);
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * 各プレイヤー運命のサマライズ。
+ * @param day 日
+ * @param sysEvent システムイベント
+ */
+ private void summarizeDestiny(int day, SysEvent sysEvent){
+ List<Avatar> avatarList = sysEvent.getAvatarList();
+ List<Integer> integerList = sysEvent.getIntegerList();
+
+ int avatarTotal = avatarList.size();
+ Avatar lastAvatar = null;
+ if(avatarTotal > 0) lastAvatar = avatarList.get(avatarTotal - 1);
+
+ SysEventType eventType = sysEvent.getSysEventType();
+ switch(eventType){
+ case EXECUTION: // G国のみ
+ if(integerList.get(avatarTotal - 1) > 0) break; // 処刑無し
+ Player executedPl = registPlayer(lastAvatar);
+ executedPl.setDestiny(Destiny.EXECUTED);
+ executedPl.setObitDay(day);
+ break;
+ case SUDDENDEATH:
+ Avatar suddenDeathAvatar = avatarList.get(0);
+ Player suddenDeathPlayer = registPlayer(suddenDeathAvatar);
+ suddenDeathPlayer.setDestiny(Destiny.SUDDENDEATH);
+ suddenDeathPlayer.setObitDay(day);
+ break;
+ case COUNTING: // G国COUNTING2は運命に関係なし
+ if(avatarTotal % 2 == 0) break; // 処刑無し
+ Player executedPlayer = registPlayer(lastAvatar);
+ executedPlayer.setDestiny(Destiny.EXECUTED);
+ executedPlayer.setObitDay(day);
+ break;
+ case MURDERED:
+ for(Avatar avatar : avatarList){
+ Player player = registPlayer(avatar);
+ player.setDestiny(Destiny.EATEN);
+ player.setObitDay(day);
+ }
+ // TODO E国ハム溶け処理は後回し
+ break;
+ default:
+ break;
+ }
+
+ return;
+ }
+
+ /**
+ * 会話時刻のサマライズ。
+ */
+ private void summarizeTime(){
+ for(Period period : this.village.getPeriodList()){
+ for(Topic topic : period.getTopicList()){
+ if( ! (topic instanceof Talk) ) continue;
+ Talk talk = (Talk) topic;
+
+ long epoch = talk.getTimeFromID();
+
+ if(this.talk1stTimeMs < 0) this.talk1stTimeMs = epoch;
+ if(this.talkLastTimeMs < 0) this.talkLastTimeMs = epoch;
+
+ if(epoch < this.talk1stTimeMs ) this.talk1stTimeMs = epoch;
+ if(epoch > this.talkLastTimeMs) this.talkLastTimeMs = epoch;
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * 占い師の活動を集計する。
+ */
+ private void summarizeJudge(){
+ List<SysEvent> eventList = this.eventMap.get(SysEventType.JUDGE);
+
+ for(SysEvent event : eventList){
+ List<Avatar> avatarList = event.getAvatarList();
+ Avatar avatar = avatarList.get(1);
+ Player seered = getPlayer(avatar);
+ GameRole role = seered.getRole();
+ switch(role){
+ case WOLF: this.ctScryWolf++; break;
+ case MADMAN: this.ctScryMadman++; break;
+ case HAMSTER: this.ctScryHamster++; break;
+ default: this.ctScryVillage++; break;
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * 占い師の活動を文字列化する。
+ * @return 占い師の活動
+ */
+ public CharSequence dumpSeerActivity(){
+ StringBuilder result = new StringBuilder();
+
+ if(this.ctScryVillage > 0){
+ result.append("村陣営を");
+ result.append(this.ctScryVillage);
+ result.append("回");
+ }
+
+ if(this.ctScryHamster > 0){
+ if(result.length() > 0) result.append('、');
+ result.append("ハムスターを");
+ result.append(this.ctScryHamster);
+ result.append("回");
+ }
+
+ if(this.ctScryMadman > 0){
+ if(result.length() > 0) result.append('、');
+ result.append("狂人を");
+ result.append(this.ctScryMadman);
+ result.append("回");
+ }
+
+ if(this.ctScryWolf > 0){
+ if(result.length() > 0) result.append('、');
+ result.append("人狼を");
+ result.append(this.ctScryWolf);
+ result.append("回");
+ }
+
+ if(result.length() <= 0) result.append("誰も占わなかった。");
+ else result.append("占った。");
+
+ CharSequence seq = WolfBBS.escapeWikiSyntax(result);
+
+ return seq;
+ }
+
+ /**
+ * 狩人の活動を集計する。
+ */
+ private void summarizeGuard(){
+ List<SysEvent> eventList;
+
+ eventList = this.eventMap.get(SysEventType.GUARD);
+ for(SysEvent event : eventList){
+ List<Avatar> avatarList = event.getAvatarList();
+ Avatar avatar = avatarList.get(1);
+ Player guarded = getPlayer(avatar);
+ GameRole guardedRole = guarded.getRole();
+ switch(guardedRole){
+ case WOLF: this.ctGuardWolf++; break;
+ case MADMAN: this.ctGuardMadman++; break;
+ case HAMSTER: this.ctGuardHamster++; break;
+ default: this.ctGuardVillage++; break;
+ }
+ }
+
+ for(Period period : this.village.getPeriodList()){
+ summarizeGjPeriod(period);
+ }
+
+ return;
+ }
+
+ /**
+ * 狩人GJの日ごとの集計。
+ * @param period 日
+ */
+ private void summarizeGjPeriod(Period period){
+ if(period.getDay() <= 2) return;
+
+ boolean hasAssaultTried = period.hasAssaultTried();
+ boolean hunterAlive = false;
+ int wolfNum = 0;
+
+ Set<Avatar> voters = period.getVoterSet();
+ for(Avatar avatar : voters){
+ Player player = getPlayer(avatar);
+ switch(player.getRole()){
+ case HUNTER: hunterAlive = true; break;
+ case WOLF: wolfNum++; break;
+ default: break;
+ }
+ }
+
+ Avatar executed = period.getExecutedAvatar();
+ if(executed != null){
+ Player player = getPlayer(executed);
+ switch(player.getRole()){
+ case HUNTER: hunterAlive = false; break;
+ case WOLF: wolfNum--; break;
+ default: break;
+ }
+ }
+
+ if( ! hunterAlive || wolfNum <= 0) return;
+
+ SysEvent sysEvent;
+
+ sysEvent = period.getTypedSysEvent(SysEventType.NOMURDER);
+ if(sysEvent == null) return;
+
+ sysEvent = period.getTypedSysEvent(SysEventType.GUARD);
+ if(sysEvent == null) return;
+
+ if(hasAssaultTried){
+ Avatar guarded = sysEvent.getAvatarList().get(1);
+ Player guardedPlayer = getPlayer(guarded);
+ GameRole guardedRole = guardedPlayer.getRole();
+ switch(guardedRole){
+ case MADMAN: this.ctGuardMadmanGJ++; break;
+ case HAMSTER: this.ctGuardHamsterGJ++; break;
+ default: this.ctGuardVillageGJ++; break;
+ }
+ }else{
+ this.ctGuardFakeGJ++; // 偽装GJ
+ }
+
+ return;
+ }
+
+ /**
+ * 狩人の活動を文字列化する。
+ * @return 狩人の活動
+ */
+ public CharSequence dumpHunterActivity(){
+ StringBuilder result = new StringBuilder();
+
+ String atLeast;
+ if(this.ctGuardFakeGJ > 0) atLeast = "少なくとも";
+ else atLeast = "";
+
+ if(this.ctGuardVillage > 0){
+ result.append(atLeast);
+ result.append("村陣営を");
+ result.append(this.ctGuardVillage);
+ result.append("回護衛し");
+ if(this.ctGuardVillageGJ > 0){
+ result.append("GJを");
+ result.append(this.ctGuardVillageGJ);
+ result.append("回出した。");
+ }else{
+ result.append("た。");
+ }
+ }
+
+ if(this.ctGuardHamster > 0){
+ result.append(atLeast);
+ result.append("ハムスターを");
+ result.append(this.ctGuardHamster);
+ result.append("回護衛し");
+ if(this.ctGuardHamsterGJ > 0){
+ result.append("GJを");
+ result.append(this.ctGuardHamsterGJ);
+ result.append("回出した。");
+ }else{
+ result.append("た。");
+ }
+ }
+
+ if(this.ctGuardMadman > 0){
+ result.append(atLeast);
+ result.append("狂人を");
+ result.append(this.ctGuardMadman);
+ result.append("回護衛し");
+ if(this.ctGuardMadmanGJ > 0){
+ result.append("GJを");
+ result.append(this.ctGuardMadmanGJ);
+ result.append("回出した。");
+ }else{
+ result.append("た。");
+ }
+ }
+
+ if(this.ctGuardWolf > 0){
+ result.append(atLeast);
+ result.append("人狼を");
+ result.append(this.ctGuardWolf);
+ result.append("回護衛した。");
+ }
+
+ if(this.ctGuardFakeGJ > 0){
+ result.append("護衛先は不明ながら偽装GJが");
+ result.append(this.ctGuardFakeGJ);
+ result.append("回あった。");
+ }
+
+ if(result.length() <= 0) result.append("誰も護衛できなかった");
+
+ CharSequence seq = WolfBBS.escapeWikiSyntax(result);
+
+ return seq;
+ }
+
+ /**
+ * 処刑概観を文字列化する。
+ * @return 文字列化した処刑概観
+ */
+ public CharSequence dumpExecutionInfo(){
+ StringBuilder result = new StringBuilder();
+
+ int exeWolf = 0;
+ int exeMad = 0;
+ int exeVillage = 0;
+ for(Player player : this.playerList){
+ Destiny destiny = player.getDestiny();
+ if(destiny != Destiny.EXECUTED) continue;
+ GameRole role = player.getRole();
+ switch(role){
+ case WOLF: exeWolf++; break;
+ case MADMAN: exeMad++; break;
+ default: exeVillage++; break;
+ }
+ }
+
+ if(exeVillage > 0){
+ result.append("▼村陣営×").append(exeVillage).append("回");
+ }
+ if(exeMad > 0){
+ if(result.length() > 0) result.append("、");
+ result.append("▼狂×").append(exeMad).append("回");
+ }
+ if(exeWolf > 0){
+ if(result.length() > 0) result.append("、");
+ result.append("▼狼×").append(exeWolf).append("回");
+ }
+ if(result.length() <= 0) result.append("なし");
+
+ CharSequence seq = WolfBBS.escapeWikiSyntax(result);
+
+ return seq;
+ }
+
+ /**
+ * 襲撃概観を文字列化する。
+ * @return 文字列化した襲撃概観
+ */
+ public CharSequence dumpAssaultInfo(){
+ StringBuilder result = new StringBuilder();
+
+ int eatMad = 0;
+ int eatVillage = 0;
+ for(Player player : this.playerList){
+ if(player.getAvatar() == Avatar.AVATAR_GERD){
+ result.append("▲ゲルト");
+ continue;
+ }
+ Destiny destiny = player.getDestiny();
+ if(destiny != Destiny.EATEN) continue;
+ GameRole role = player.getRole();
+ switch(role){
+ case MADMAN: eatMad++; break;
+ default: eatVillage++; break;
+ }
+ }
+
+ if(eatVillage > 0){
+ if(result.length() > 0) result.append("、");
+ result.append("▲村陣営×").append(eatVillage).append("回");
+ }
+ if(eatMad > 0){
+ if(result.length() > 0) result.append("、");
+ result.append("▲狂×").append(eatMad).append("回");
+ }
+
+ if(result.length() <= 0) result.append("襲撃なし");
+
+ CharSequence seq = WolfBBS.escapeWikiSyntax(result);
+
+ return seq;
+ }
+
+ /**
+ * まとめサイト用投票Boxを生成する。
+ * @return 投票BoxのWikiテキスト
+ */
+ public CharSequence dumpVoteBox(){
+ StringBuilder wikiText = new StringBuilder();
+
+ for(Player player : getCastingPlayerList()){
+ Avatar avatar = player.getAvatar();
+ if(avatar == Avatar.AVATAR_GERD) continue;
+ GameRole role = player.getRole();
+ CharSequence fullName = avatar.getFullName();
+ CharSequence roleName = role.getRoleName();
+ StringBuilder line = new StringBuilder();
+ line.append("[").append(roleName).append("] ").append(fullName);
+ if(wikiText.length() > 0) wikiText.append(',');
+ wikiText.append(WolfBBS.escapeWikiSyntax(line));
+ wikiText.append("[0]");
+ }
+
+ wikiText.insert(0, "#vote(").append(")\n");
+
+ return wikiText;
+ }
+
+ /**
+ * まとめサイト用キャスト表を生成する。
+ * @param iconSet 顔アイコンセット
+ * @return キャスト表のWikiテキスト
+ */
+ public CharSequence dumpCastingBoard(FaceIconSet iconSet){
+ StringBuilder wikiText = new StringBuilder();
+
+ String vName = this.village.getVillageFullName();
+ String author = iconSet.getAuthor() + "氏"
+ +" [ "+iconSet.getUrlText()+" ]";
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("// ↓キャスト表開始\n");
+ wikiText.append("// Village : ")
+ .append(vName)
+ .append('\n');
+ wikiText.append("// Generator : ")
+ .append(GENERATOR)
+ .append('\n');
+ wikiText.append("// アイコン作者 : ")
+ .append(author)
+ .append('\n');
+ wikiText.append("// ※アイコン画像の著作財産権保持者")
+ .append("および画像サーバ運営者から\n");
+ wikiText.append("// 新しい意向が示された場合、")
+ .append("そちらを最優先で尊重してください。\n");
+ wikiText.append(WolfBBS.COMMENTLINE);
+
+ wikiText.append("|配役")
+ .append("|参加者")
+ .append("|役職")
+ .append("|運命")
+ .append("|その活躍")
+ .append("|h")
+ .append('\n');
+ wikiText.append(WolfBBS.COMMENTLINE);
+
+ boolean even = true;
+
+ for(Player player : getCastingPlayerList()){
+ Avatar avatar = player.getAvatar();
+ GameRole role = player.getRole();
+ Destiny destiny = player.getDestiny();
+ int obitDay = player.getObitDay();
+ String name = player.getIdName();
+ String urlText = player.getUrlText();
+ if(urlText == null) urlText = "";
+ urlText = urlText.replace("~", "%7e");
+ urlText = urlText.replace(" ", "%20");
+ try{
+ URL url = new URL(urlText);
+ URI uri = url.toURI();
+ urlText = uri.toASCIIString();
+ }catch(MalformedURLException | URISyntaxException e){
+ // NOTHING
+ }
+ // PukiWikiではURL内の&のエスケープは不要?
+
+ wikiText.append("// ========== ");
+ wikiText.append(name)
+ .append(" acts as [")
+ .append(avatar.getName())
+ .append("]");
+ wikiText.append(" ==========\n");
+
+ Color teamColor = WolfBBS.getTeamColor(role);
+ Color destinyColor = WolfBBS.getDestinyColor(destiny);
+ Color plainColor = COLOR_PLAINTABLE;
+ if(even){
+ teamColor = WolfBBS.evenColor(teamColor);
+ destinyColor = WolfBBS.evenColor(destinyColor);
+ plainColor = WolfBBS.evenColor(plainColor);
+ }
+ even = ! even;
+
+ String teamWikiColor = "BGCOLOR("
+ + WolfBBS.cnvWikiColor(teamColor)
+ + "):";
+ String destinyWikiColor = "BGCOLOR("
+ + WolfBBS.cnvWikiColor(destinyColor)
+ + "):";
+ String plainWikiColor = "BGCOLOR("
+ + WolfBBS.cnvWikiColor(plainColor)
+ + "):";
+
+ String avatarIcon = iconSet.getAvatarIconWiki(avatar);
+
+ wikiText.append('|').append(teamWikiColor);
+ wikiText.append(avatarIcon).append("&br;");
+
+ wikiText.append("[[").append(avatar.getName()).append("]]");
+
+ wikiText.append('|').append(teamWikiColor);
+ wikiText.append("[[").append(WolfBBS.escapeWikiBracket(name));
+ if(urlText != null && urlText.length() > 0){
+ wikiText.append('>').append(urlText);
+ }
+ wikiText.append("]]");
+
+ wikiText.append('|').append(teamWikiColor);
+ wikiText.append(WolfBBS.getRoleIconWiki(role));
+ wikiText.append("&br;");
+ wikiText.append("[[");
+ wikiText.append(role.getRoleName());
+ wikiText.append("]]");
+
+ wikiText.append('|').append(destinyWikiColor);
+ if(destiny == Destiny.ALIVE){
+ wikiText.append("最後まで&br;生存");
+ }else{
+ wikiText.append(obitDay).append("日目").append("&br;");
+ wikiText.append(destiny.getMessage());
+ }
+
+ wikiText.append('|').append(plainWikiColor);
+ wikiText.append(avatar.getJobTitle()).append('。');
+
+ if(avatar == Avatar.AVATAR_GERD){
+ wikiText.append("寝てばかりいた。");
+ }else if(role == GameRole.HUNTER){
+ CharSequence report = dumpHunterActivity();
+ wikiText.append(report);
+ }else if(role == GameRole.SEER){
+ CharSequence report = dumpSeerActivity();
+ wikiText.append(report);
+ }
+
+ wikiText.append("|\n");
+
+ }
+
+ wikiText.append("|>|>|>|>|");
+ wikiText.append("RIGHT:");
+ wikiText.append("顔アイコン提供 : [[");
+ wikiText.append(WolfBBS.escapeWikiBracket(iconSet.getAuthor()));
+ wikiText.append(">")
+ .append(iconSet.getUrlText());
+ wikiText.append("]]氏");
+ wikiText.append("|\n");
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("// ↑キャスト表ここまで\n");
+ wikiText.append(WolfBBS.COMMENTLINE);
+
+ return wikiText;
+ }
+
+ /**
+ * 村詳細情報を出力する。
+ * @return 村詳細情報
+ */
+ public CharSequence dumpVillageWiki(){
+ StringBuilder wikiText = new StringBuilder();
+
+ DateFormat dform =
+ DateFormat.getDateTimeInstance(DateFormat.FULL,
+ DateFormat.FULL);
+
+ String vName = this.village.getVillageFullName();
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("// ↓村詳細開始\n");
+ wikiText.append("// Village : ")
+ .append(vName)
+ .append('\n');
+ wikiText.append("// Generator : ")
+ .append(GENERATOR)
+ .append('\n');
+
+ wikiText.append("* 村の詳細\n");
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 勝者\n");
+ Team winnerTeam = getWinnerTeam();
+ String wonTeam = winnerTeam.getTeamName();
+ wikiText.append(wonTeam).append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- エントリー開始時刻\n");
+ Date date = get1stTalkDate();
+ String talk1st = dform.format(date);
+ wikiText.append(talk1st).append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 参加人数\n");
+ int avatarNum = countAvatarNum();
+ String totalMember = "ゲルト + " + (avatarNum - 1) + "名 = "
+ + avatarNum + "名";
+ wikiText.append(WolfBBS.escapeWikiSyntax(totalMember))
+ .append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 役職内訳\n");
+ StringBuilder roleMsg = new StringBuilder();
+ for(GameRole role : GameRole.values()){
+ List<Player> players = getRoledPlayerList(role);
+ String roleName = role.getRoleName();
+ if(players.size() <= 0) continue;
+ if(roleMsg.length() > 0) roleMsg.append('、');
+ roleMsg.append(roleName)
+ .append(" × ")
+ .append(players.size())
+ .append("名");
+ }
+ wikiText.append(WolfBBS.escapeWikiSyntax(roleMsg)).append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 処刑内訳\n");
+ wikiText.append(dumpExecutionInfo()).append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 襲撃内訳\n");
+ wikiText.append(dumpAssaultInfo()).append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 突然死\n");
+ wikiText.append(countSuddenDeath()).append("名").append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 人口推移\n");
+ for(int day = 1; day < this.village.getPeriodSize(); day++){
+ List<Player> players = getSurvivorList(day);
+ CharSequence roleSeq =
+ GameSummary.getRoleBalanceSequence(players);
+ String daySeq;
+ Period period = this.village.getPeriod(day);
+ daySeq = period.getCaption();
+ wikiText.append('|')
+ .append(daySeq)
+ .append('|')
+ .append(roleSeq)
+ .append("|\n");
+ }
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 占い師の成績\n");
+ wikiText.append(dumpSeerActivity()).append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("- 狩人の成績\n");
+ wikiText.append(dumpHunterActivity()).append('\n');
+
+ wikiText.append(WolfBBS.COMMENTLINE);
+ wikiText.append("// ↑村詳細ここまで\n");
+ wikiText.append(WolfBBS.COMMENTLINE);
+
+ return wikiText;
+ }
+
+ /**
+ * 最初の発言の時刻を得る。
+ * @return 時刻
+ */
+ public Date get1stTalkDate(){
+ return new Date(this.talk1stTimeMs);
+ }
+
+ /**
+ * 最後の発言の時刻を得る。
+ * @return 時刻
+ */
+ public Date getLastTalkDate(){
+ return new Date(this.talkLastTimeMs);
+ }
+
+ /**
+ * 指定した日の生存者一覧を得る。
+ * @param day 日
+ * @return 生存者一覧
+ */
+ public List<Player> getSurvivorList(int day){
+ if(day < 0 || this.village.getPeriodSize() <= day){
+ throw new IndexOutOfBoundsException();
+ }
+
+ List<Player> result = new LinkedList<>();
+
+ Period period = this.village.getPeriod(day);
+
+ if( period.isPrologue()
+ || (period.isProgress() && day == 1) ){
+ result.addAll(this.playerList);
+ return result;
+ }
+
+ if(period.isEpilogue()){
+ for(Player player : this.playerList){
+ if(player.getDestiny() == Destiny.ALIVE){
+ result.add(player);
+ }
+ }
+ return result;
+ }
+
+ for(Topic topic : period.getTopicList()){
+ if( ! (topic instanceof SysEvent) ) continue;
+ SysEvent sysEvent = (SysEvent) topic;
+ if(sysEvent.getSysEventType() == SysEventType.SURVIVOR){
+ List<Avatar> avatarList = sysEvent.getAvatarList();
+ for(Avatar avatar : avatarList){
+ Player player = getPlayer(avatar);
+ result.add(player);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * プレイヤー一覧を得る。
+ * 参加エントリー順
+ * @return プレイヤーのリスト
+ */
+ public List<Player> getPlayerList(){
+ List<Player> result = Collections.unmodifiableList(this.playerList);
+ return result;
+ }
+
+ /**
+ * キャスティング表用にソートされたプレイヤー一覧を得る。
+ * @return プレイヤーのリスト
+ */
+ public List<Player> getCastingPlayerList(){
+ List<Player> sortedPlayers =
+ new LinkedList<>();
+ sortedPlayers.addAll(this.playerList);
+ Collections.sort(sortedPlayers, COMPARATOR_CASTING);
+ return sortedPlayers;
+ }
+
+ /**
+ * 指定された役職のプレイヤー一覧を得る。
+ * @param role 役職
+ * @return 役職に合致するプレイヤーのリスト
+ */
+ public List<Player> getRoledPlayerList(GameRole role){
+ List<Player> result = new LinkedList<>();
+
+ for(Player player : this.playerList){
+ if(player.getRole() == role){
+ result.add(player);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 勝利陣営を得る。
+ * @return 勝利した陣営
+ */
+ public Team getWinnerTeam(){
+ return this.winner;
+ }
+
+ /**
+ * 突然死者数を得る。
+ * @return 突然死者数
+ */
+ public int countSuddenDeath(){
+ int suddenDeath = 0;
+ for(Player player : this.playerList){
+ if(player.getDestiny() == Destiny.SUDDENDEATH) suddenDeath++;
+ }
+ return suddenDeath;
+ }
+
+ /**
+ * 参加プレイヤー総数を得る。
+ * @return プレイヤー総数
+ */
+ public int countAvatarNum(){
+ int playerNum = this.playerList.size();
+ return playerNum;
+ }
+
+ /**
+ * AvatarからPlayerを得る。
+ * 参加していないAvatarならnullを返す。
+ * @param avatar Avatar
+ * @return Player
+ */
+ public final Player getPlayer(Avatar avatar){
+ Player player = this.playerMap.get(avatar);
+ return player;
+ }
+
+ /**
+ * AvatarからPlayerを得る。
+ * 無ければ新規に作る。
+ * @param avatar Avatar
+ * @return Player
+ */
+ private Player registPlayer(Avatar avatar){
+ Player player = getPlayer(avatar);
+ if(player == null){
+ player = new Player();
+ player.setAvatar(avatar);
+ this.playerMap.put(avatar, player);
+ }
+ return player;
+ }
+
+ /**
+ * プレイヤーのソート仕様の記述。
+ * まとめサイトのキャスト表向け。
+ */
+ private static final class CastingComparator
+ implements Comparator<Player> {
+
+ /**
+ * コンストラクタ。
+ */
+ private CastingComparator(){
+ super();
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param p1 {@inheritDoc}
+ * @param p2 {@inheritDoc}
+ * @return {@inheritDoc}
+ */
+ @Override
+ public int compare(Player p1, Player p2){
+ if(p1 == p2) return 0;
+ if(p1 == null) return -1;
+ if(p2 == null) return +1;
+
+ // ゲルトが最前
+ Avatar avatar1 = p1.getAvatar();
+ Avatar avatar2 = p2.getAvatar();
+ if(avatar1.equals(avatar2)) return 0;
+ if(avatar1 == Avatar.AVATAR_GERD) return -1;
+ if(avatar2 == Avatar.AVATAR_GERD) return +1;
+
+ // 生存者は最後
+ Destiny dest1 = p1.getDestiny();
+ Destiny dest2 = p2.getDestiny();
+ if(dest1 != dest2){
+ if (dest1 == Destiny.ALIVE) return +1;
+ else if(dest2 == Destiny.ALIVE) return -1;
+ }
+
+ // 退場順
+ int obitDay1 = p1.getObitDay();
+ int obitDay2 = p2.getObitDay();
+ if(obitDay1 > obitDay2) return +1;
+ if(obitDay1 < obitDay2) return -1;
+
+ // 運命順
+ int destinyOrder = dest1.compareTo(dest2);
+ if(destinyOrder != 0) return destinyOrder;
+
+ // エントリー順
+ int entryOrder = p1.getEntryNo() - p2.getEntryNo();
+
+ return entryOrder;
+ }
+ }
+
+ // TODO E国ハムスター対応
+}