├── run.bat ├── fantypes.xlsx ├── run.sh ├── .gitignore ├── src ├── main │ ├── resources │ │ ├── logging_botcompetition.properties │ │ ├── logging.properties │ │ └── message.properties │ └── java │ │ └── com │ │ └── github │ │ └── blovemaple │ │ └── mj │ │ ├── action │ │ ├── AutoActionType.java │ │ ├── IllegalActionException.java │ │ ├── StageSwitchAction.java │ │ ├── PlayerActionType.java │ │ ├── standard │ │ │ ├── LiujuActionType.java │ │ │ ├── DrawBottomActionType.java │ │ │ ├── StageSwitchActionType.java │ │ │ ├── AutoActionTypes.java │ │ │ ├── DealActionType.java │ │ │ ├── AngangActionType.java │ │ │ ├── DiscardActionType.java │ │ │ ├── DrawActionType.java │ │ │ ├── DiscardWithTingActionType.java │ │ │ ├── BuhuaActionType.java │ │ │ ├── BugangActionType.java │ │ │ ├── PlayerActionTypes.java │ │ │ ├── WinActionType.java │ │ │ └── CpgActionType.java │ │ ├── Action.java │ │ ├── ActionType.java │ │ ├── ActionTypeAndLocation.java │ │ ├── PlayerAction.java │ │ └── AbstractPlayerActionType.java │ │ ├── rule │ │ ├── win │ │ │ ├── FanTypeMatcher.java │ │ │ ├── FanType.java │ │ │ ├── CachedWinFeature.java │ │ │ ├── WinType.java │ │ │ └── WinInfo.java │ │ ├── TimeLimitStrategy.java │ │ ├── simple │ │ │ ├── SimpleFanType.java │ │ │ ├── SimpleTimeLimitStrategy.java │ │ │ ├── FinishedStage.java │ │ │ ├── DealingStage.java │ │ │ ├── BeforePlayingStage.java │ │ │ ├── SimpleGameStrategy.java │ │ │ └── PlayingStage.java │ │ ├── GameStage.java │ │ ├── InitStage.java │ │ ├── GameStrategy.java │ │ └── AbstractGameStrategy.java │ │ ├── object │ │ ├── TileUnitType.java │ │ ├── PlayerInfoPlayerView.java │ │ ├── TileGroupPlayerView.java │ │ ├── MahjongTablePlayerView.java │ │ ├── PlayerTiles.java │ │ ├── PlayerLocation.java │ │ ├── TileGroupType.java │ │ ├── TileSuit.java │ │ ├── TileRank.java │ │ ├── Tile.java │ │ ├── TileUnit.java │ │ ├── Player.java │ │ ├── TileType.java │ │ ├── PlayerInfo.java │ │ ├── TileGroup.java │ │ └── MahjongTable.java │ │ ├── game │ │ ├── GameContext.java │ │ ├── GameContextPlayerView.java │ │ ├── GameResult.java │ │ ├── GameContextPlayerViewImpl.java │ │ └── GameContextImpl.java │ │ ├── local │ │ ├── TestBot.java │ │ ├── bazbot │ │ │ ├── BazBot.java │ │ │ ├── BazBotAliveTiles.java │ │ │ ├── BazBotChoosingTileUnits.java │ │ │ ├── BazBotTileUnit.java │ │ │ ├── BazBotTileUnits.java │ │ │ └── BazBotTileNeighborhood.java │ │ ├── LocalGame.java │ │ ├── barbot │ │ │ ├── BarBot.java │ │ │ ├── BarBotSimChanging.java │ │ │ └── BarBotSimContext.java │ │ └── AbstractBot.java │ │ ├── utils │ │ ├── LambdaUtils.java │ │ └── LanguageManager.java │ │ ├── cli │ │ ├── CliRunner.java │ │ └── CliView.java │ │ └── botcompetition │ │ └── BotCompetition.java └── test │ └── java │ └── com │ └── github │ └── blovemaple │ └── mj │ └── local │ ├── bazbot │ └── BazBotTest.java │ └── foobot │ ├── SimpleGameStrategyTest.java │ └── NormalWinTypeTest.java ├── README.mediawiki └── pom.xml /run.bat: -------------------------------------------------------------------------------- 1 | java -jar mahjong-0.0.1-SNAPSHOT-jar-with-dependencies.jar -------------------------------------------------------------------------------- /fantypes.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blovemaple/mahjong/HEAD/fantypes.xlsx -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | java -jar target/mahjong-0.0.1-SNAPSHOT-jar-with-dependencies.jar 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.jar 2 | *.war 3 | *.ear 4 | 5 | *.class 6 | .classpath 7 | .project 8 | .settings 9 | /target/ 10 | mahjong.log 11 | -------------------------------------------------------------------------------- /src/main/resources/logging_botcompetition.properties: -------------------------------------------------------------------------------- 1 | .level=WARNING 2 | 3 | handlers=java.util.logging.ConsoleHandler 4 | 5 | java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [%4$s] [%3$s] %5$s%6$s%n -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/AutoActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | /** 4 | * 由游戏自动做出的动作类型。 5 | * 6 | * @author blovemaple 7 | */ 8 | public interface AutoActionType extends ActionType { 9 | } 10 | -------------------------------------------------------------------------------- /README.mediawiki: -------------------------------------------------------------------------------- 1 | ==这是什么== 2 | 用Java写的单机命令行麻将游戏。 3 | 4 | 点击直接玩(powered by [https://github.com/yudai/gotty gotty]):http://chentong.ren:8888/ 5 | 6 | 待实现: 7 | #番种 8 | #ANSI格式输出 9 | 10 | ==如何运行== 11 | 入口类: 12 | com.github.blovemaple.mj.cli.CliRunner 13 | 编译后可执行jar包: 14 | mahjong-*-jar-with-dependencies.jar 15 | -------------------------------------------------------------------------------- /src/main/resources/logging.properties: -------------------------------------------------------------------------------- 1 | .level=INFO 2 | 3 | handlers=java.util.logging.FileHandler 4 | 5 | java.util.logging.FileHandler.level=ALL 6 | java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter 7 | java.util.logging.FileHandler.pattern=mahjong.log 8 | java.util.logging.FileHandler.append=true 9 | 10 | java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [%4$s] [%3$s] %5$s%6$s%n -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/win/FanTypeMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.win; 2 | 3 | /** 4 | * 判断是否符合番种的接口,定义一个完整的判断条件。 5 | * 6 | * @author blovemaple 7 | */ 8 | public interface FanTypeMatcher { 9 | /** 10 | * 检查和牌是否符合此番种,返回计入次数。如果检查过程中检查出了符合其他番种,应填入winInfo.fans。 11 | * 12 | * @param winInfo 13 | * 和牌信息。 14 | * @return 如果不符合,返回0;如果符合,返回应该计入的次数。 15 | */ 16 | public int matchCount(WinInfo winInfo); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileUnitType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.Collection; 4 | 5 | /** 6 | * 牌的单元的类型。 7 | * 8 | * @author blovemaple 9 | */ 10 | public interface TileUnitType { 11 | 12 | /** 13 | * 返回一个单元中有几张牌。 14 | */ 15 | int size(); 16 | 17 | /** 18 | * 判断指定牌集合是否是合法的单元。 19 | */ 20 | boolean isLegalTiles(Collection tiles); 21 | 22 | /** 23 | * 判断指定牌型集合是否是合法的单元。 24 | */ 25 | boolean isLegalTileTypes(Collection types); 26 | 27 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/IllegalActionException.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | import com.github.blovemaple.mj.game.GameContext; 4 | 5 | /** 6 | * 尝试执行非法动作时抛出此异常。 7 | * 8 | * @author blovemaple 9 | */ 10 | public class IllegalActionException extends Exception { 11 | private static final long serialVersionUID = 1L; 12 | 13 | @SuppressWarnings("unused") 14 | private Action action; 15 | 16 | public IllegalActionException(GameContext context, Action action) { 17 | super(action.toString() + " context: " + context); 18 | this.action = action; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/PlayerInfoPlayerView.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * {@link PlayerInfo}给其他玩家的视图接口。 7 | * 8 | * @author blovemaple 9 | */ 10 | public interface PlayerInfoPlayerView { 11 | /** 12 | * 返回玩家名称。 13 | */ 14 | public String getPlayerName(); 15 | 16 | /** 17 | * 返回手中的牌数。 18 | */ 19 | public int getAliveTileSize(); 20 | 21 | /** 22 | * 返回已经打出的牌。 23 | */ 24 | public List getDiscardedTiles(); 25 | 26 | /** 27 | * 返回牌组视图列表。 28 | */ 29 | public List getTileGroups(); 30 | 31 | /** 32 | * 返回是否听和。 33 | */ 34 | public boolean isTing(); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/StageSwitchAction.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | import com.github.blovemaple.mj.action.standard.StageSwitchActionType; 4 | 5 | /** 6 | * 切换阶段的动作。 7 | * 8 | * @author blovemaple 9 | */ 10 | public class StageSwitchAction extends Action { 11 | 12 | private final String nextStageName; 13 | 14 | public StageSwitchAction(String nextStageName) { 15 | super(StageSwitchActionType.INSTANCE); 16 | this.nextStageName = nextStageName; 17 | } 18 | 19 | public String getNextStageName() { 20 | return nextStageName; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "[STAGE " + nextStageName + "]"; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/TimeLimitStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | 6 | import com.github.blovemaple.mj.action.PlayerActionType; 7 | import com.github.blovemaple.mj.game.GameContext; 8 | import com.github.blovemaple.mj.object.PlayerLocation; 9 | 10 | /** 11 | * 限时策略。 12 | * 13 | * @author blovemaple 14 | */ 15 | @FunctionalInterface 16 | public interface TimeLimitStrategy { 17 | /** 18 | * 不限时。 19 | */ 20 | public static final TimeLimitStrategy NO_LIMIT = (context, choises) -> null; 21 | 22 | /** 23 | * 根据上下文返回限时。 24 | * 25 | * @return 限时(单位:秒),若不限制则返回null。 26 | */ 27 | Integer getLimit(GameContext context, 28 | Map> choises); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/simple/SimpleFanType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.simple; 2 | 3 | import java.util.Set; 4 | 5 | import com.github.blovemaple.mj.rule.win.FanType; 6 | import com.github.blovemaple.mj.rule.win.WinInfo; 7 | 8 | /** 9 | * 简单番种,和牌即算。 10 | * 11 | * @author blovemaple 12 | */ 13 | public class SimpleFanType implements FanType { 14 | public static final String NAME = "SIMPLE"; 15 | 16 | @Override 17 | public String name() { 18 | return NAME; 19 | } 20 | 21 | @Override 22 | public int matchCount(WinInfo winInfo) { 23 | return 1; 24 | } 25 | 26 | @Override 27 | public int score() { 28 | return 1; 29 | } 30 | 31 | @Override 32 | public Set covered() { 33 | return null; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileGroupPlayerView.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.Set; 4 | 5 | import com.github.blovemaple.mj.object.PlayerLocation.Relation; 6 | 7 | /** 8 | * {@link TileGroup}给其他玩家的视图接口。 9 | * 10 | * @author blovemaple 11 | */ 12 | public interface TileGroupPlayerView { 13 | 14 | /** 15 | * 返回类型。 16 | * 17 | * @return 类型 18 | */ 19 | public TileGroupType getType(); 20 | 21 | /** 22 | * 返回牌组中所有牌。类型为暗杠时返回null。 23 | * 24 | * @return tiles 集合 25 | */ 26 | public Set getTiles(); 27 | 28 | /** 29 | * 返回得牌来自于哪个关系的玩家。 30 | * 31 | * @return 玩家位置 32 | */ 33 | public Relation getFromRelation(); 34 | 35 | /** 36 | * 返回得牌。类型为暗杠时返回null。 37 | * 38 | * @return 得牌 39 | */ 40 | public Tile getGotTile() ; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/MahjongTablePlayerView.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * {@link MahjongTable}给一个玩家的视图接口。 7 | * 8 | * @author blovemaple 9 | */ 10 | public interface MahjongTablePlayerView { 11 | 12 | /** 13 | * 返回当前玩家位置。 14 | */ 15 | public PlayerLocation getMyLocation(); 16 | 17 | /** 18 | * 返回指定位置的玩家名称。 19 | */ 20 | public String getPlayerName(PlayerLocation location); 21 | 22 | /** 23 | * 返回牌墙中的剩余牌数。 24 | */ 25 | public int getTileWallSize(); 26 | 27 | /** 28 | * 返回此局开始时的底牌数量。 29 | */ 30 | public int getInitBottomSize(); 31 | 32 | /** 33 | * 返回已经从底部摸牌的数量。 34 | */ 35 | public int getDrawedBottomSize(); 36 | 37 | /** 38 | * 返回PlayerInfo视图。 39 | */ 40 | public Map getPlayerInfoView(); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/PlayerTiles.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | /** 9 | * 一个玩家的牌。 10 | * 11 | * @author blovemaple 12 | */ 13 | public class PlayerTiles { 14 | /** 15 | * 手中的牌。 16 | */ 17 | protected Set aliveTiles = new HashSet<>(); 18 | /** 19 | * 吃碰杠。 20 | */ 21 | protected List tileGroups = new ArrayList<>(); 22 | 23 | public PlayerTiles() { 24 | super(); 25 | } 26 | 27 | public Set getAliveTiles() { 28 | return aliveTiles; 29 | } 30 | 31 | public void setAliveTiles(Set aliveTiles) { 32 | this.aliveTiles = aliveTiles; 33 | } 34 | 35 | public List getTileGroups() { 36 | return tileGroups; 37 | } 38 | 39 | public void setTileGroups(List tileGroups) { 40 | this.tileGroups = tileGroups; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/PlayerActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | import java.util.Collection; 4 | import java.util.Set; 5 | 6 | import com.github.blovemaple.mj.game.GameContext; 7 | import com.github.blovemaple.mj.game.GameContextPlayerView; 8 | import com.github.blovemaple.mj.object.PlayerLocation; 9 | import com.github.blovemaple.mj.object.Tile; 10 | 11 | /** 12 | * 由玩家做出的动作类型。 13 | * 14 | * @author blovemaple 15 | */ 16 | public interface PlayerActionType extends ActionType { 17 | 18 | /** 19 | * 判断指定状态下指定位置的玩家可否做此种类型的动作。 20 | * 21 | * @return 能做返回true;否则返回false。 22 | */ 23 | public boolean canDo(GameContext context, PlayerLocation location); 24 | 25 | /** 26 | * 返回此动作是否可以放弃。 27 | * 28 | * @return 可以放弃返回true;否则返回false。 29 | */ 30 | public boolean canPass(GameContext context, PlayerLocation location); 31 | 32 | /** 33 | * 返回一个集合,包含指定状态下指定玩家可作出的此类型的所有合法动作的相关牌集合。 34 | */ 35 | Collection> getLegalActionTiles(GameContextPlayerView context); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/LiujuActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import com.github.blovemaple.mj.action.Action; 4 | import com.github.blovemaple.mj.action.AutoActionType; 5 | import com.github.blovemaple.mj.action.IllegalActionException; 6 | import com.github.blovemaple.mj.game.GameContext; 7 | import com.github.blovemaple.mj.game.GameResult; 8 | import com.github.blovemaple.mj.rule.simple.PlayingStage; 9 | 10 | /** 11 | * 动作类型“流局”。 12 | * 13 | * @author blovemaple 14 | */ 15 | public class LiujuActionType implements AutoActionType { 16 | 17 | protected LiujuActionType() { 18 | } 19 | 20 | @Override 21 | public boolean isLegalAction(GameContext context, Action action) { 22 | return context.getStage().getName().equals(PlayingStage.NAME); 23 | } 24 | 25 | @Override 26 | public void doAction(GameContext context, Action action) throws IllegalActionException { 27 | GameResult result = new GameResult(context.getTable().getPlayerInfos(), 28 | context.getZhuangLocation()); 29 | context.setGameResult(result); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/PlayerLocation.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | /** 4 | * 玩家位置。 5 | * 6 | * @author blovemaple 7 | */ 8 | public enum PlayerLocation { 9 | // 顺序勿动。计算位置依赖此枚举的顺序! 10 | EAST, NORTH, WEST, SOUTH; 11 | 12 | /** 13 | * 位置关系。 14 | * 15 | * @author blovemaple 16 | */ 17 | public enum Relation { 18 | // 顺序勿动。计算位置依赖此枚举的顺序! 19 | SELF, NEXT, OPPOSITE, PREVIOUS; 20 | 21 | /** 22 | * 判断是否是其他人(非SELF)。 23 | */ 24 | public boolean isOther() { 25 | return this != SELF; 26 | } 27 | } 28 | 29 | /** 30 | * 返回另一个位置相对于此位置的关系。 31 | * 32 | * @param other 33 | * 另一个位置 34 | * @return 关系 35 | */ 36 | public Relation getRelationOf(PlayerLocation other) { 37 | int dis = other.ordinal() - this.ordinal(); 38 | if (dis < 0) 39 | dis += 4; 40 | return Relation.values()[dis]; 41 | } 42 | 43 | /** 44 | * 返回相对于此位置的指定关系的位置。 45 | * 46 | * @param relation 47 | * 关系 48 | * @return 位置 49 | */ 50 | public PlayerLocation getLocationOf(Relation relation) { 51 | return PlayerLocation.values()[(this.ordinal() + relation.ordinal()) 52 | % 4]; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileGroupType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import static com.github.blovemaple.mj.object.StandardTileUnitType.*; 4 | 5 | import java.util.Set; 6 | 7 | /** 8 | * 牌组类型。 9 | * 10 | * @author blovemaple 11 | */ 12 | public enum TileGroupType { 13 | /** 14 | * 吃 15 | */ 16 | CHI_GROUP(SHUNZI), 17 | /** 18 | * 碰 19 | */ 20 | PENG_GROUP(KEZI), 21 | /** 22 | * 直杠 23 | */ 24 | ZHIGANG_GROUP(GANGZI), 25 | /** 26 | * 补杠 27 | */ 28 | BUGANG_GROUP(GANGZI), 29 | /** 30 | * 暗杠 31 | */ 32 | ANGANG_GROUP(GANGZI), 33 | /** 34 | * 补花 35 | */ 36 | BUHUA_GROUP(HUA_UNIT); 37 | 38 | private final TileUnitType unitType; 39 | 40 | private TileGroupType(TileUnitType unitType) { 41 | this.unitType = unitType; 42 | } 43 | 44 | public TileUnitType getUnitType() { 45 | return unitType; 46 | } 47 | 48 | /** 49 | * 返回一个单元中有几张牌。 50 | */ 51 | public int size() { 52 | return unitType.size(); 53 | } 54 | 55 | /** 56 | * 判断指定牌集合是否是合法的牌组。 57 | */ 58 | public boolean isLegalTiles(Set tiles) { 59 | return unitType.isLegalTiles(tiles); 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/simple/SimpleTimeLimitStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.simple; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import com.github.blovemaple.mj.action.PlayerActionType; 8 | import com.github.blovemaple.mj.game.GameContext; 9 | import com.github.blovemaple.mj.object.PlayerLocation; 10 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 11 | 12 | /** 13 | * 简单的限时策略,采用固定限时。 14 | * 15 | * @author blovemaple 16 | */ 17 | public class SimpleTimeLimitStrategy implements TimeLimitStrategy { 18 | private final int limit; 19 | 20 | /** 21 | * 新建一个实例。 22 | * 23 | * @param discardLimit 24 | * 打牌限时 25 | * @param cpkLimit 26 | * 其他操作限时 27 | * @param timeUnit 28 | * 时间单位 29 | */ 30 | public SimpleTimeLimitStrategy(int limit, TimeUnit timeUnit) { 31 | this.limit = (int) TimeUnit.SECONDS.convert(limit, timeUnit); 32 | } 33 | 34 | @Override 35 | public Integer getLimit(GameContext context, 36 | Map> choises) { 37 | return limit; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/GameStage.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule; 2 | 3 | import java.util.List; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.action.AutoActionType; 7 | import com.github.blovemaple.mj.action.PlayerActionType; 8 | import com.github.blovemaple.mj.game.GameContext; 9 | 10 | /** 11 | * 游戏阶段。定义阶段中玩家可使用的所有动作类型,以及转换到其他阶段的条件。 12 | * 13 | * @author blovemaple 14 | */ 15 | public interface GameStage { 16 | 17 | /** 18 | * 返回该阶段名称。 19 | */ 20 | public String getName(); 21 | 22 | /** 23 | * 返回该阶段中玩家可使用的所有动作类型。 24 | */ 25 | public List getPlayerActionTypes(); 26 | 27 | /** 28 | * 返回该阶段中可自动做出的所有动作类型。 29 | */ 30 | public List getAutoActionTypes(); 31 | 32 | /** 33 | * 根据当前状态决定阶段动作(如切换到其他阶段)并返回,不执行动作时返回null。
34 | * 此方法返回的动作具有最高优先级。 35 | */ 36 | public Action getPriorAction(GameContext context); 37 | 38 | /** 39 | * 返回当玩家和自动动作类型都无动作可做时应该执行的动作(如切换到其他阶段)。
40 | * 此方法返回的动作具有最低优先级。 41 | */ 42 | public Action getFinalAction(GameContext context); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/simple/FinishedStage.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.simple; 2 | 3 | import java.util.List; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.action.AutoActionType; 7 | import com.github.blovemaple.mj.action.PlayerActionType; 8 | import com.github.blovemaple.mj.game.GameContext; 9 | import com.github.blovemaple.mj.rule.GameStage; 10 | 11 | /** 12 | * 一局游戏结束后的阶段。 13 | * 14 | * @author blovemaple 15 | */ 16 | public class FinishedStage implements GameStage { 17 | public static final String NAME = "FINISHED"; 18 | 19 | @Override 20 | public String getName() { 21 | return NAME; 22 | } 23 | 24 | @Override 25 | public List getPlayerActionTypes() { 26 | return List.of(); 27 | } 28 | 29 | @Override 30 | public List getAutoActionTypes() { 31 | return List.of(); 32 | } 33 | 34 | @Override 35 | public Action getPriorAction(GameContext context) { 36 | return null; 37 | } 38 | 39 | @Override 40 | public Action getFinalAction(GameContext context) { 41 | // TODO Auto-generated method stub 42 | return null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/Action.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | /** 4 | * 动作。 5 | * 6 | * @param 7 | * 附加信息类型 8 | * @author blovemaple 9 | */ 10 | public class Action { 11 | /** 12 | * 动作类型。 13 | */ 14 | private ActionType type; 15 | 16 | /** 17 | * 新建一个实例。 18 | */ 19 | public Action(ActionType type) { 20 | this.type = type; 21 | } 22 | 23 | public ActionType getType() { 24 | return type; 25 | } 26 | 27 | @Override 28 | public int hashCode() { 29 | final int prime = 31; 30 | int result = 1; 31 | result = prime * result + ((type == null) ? 0 : type.hashCode()); 32 | return result; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object obj) { 37 | if (this == obj) 38 | return true; 39 | if (obj == null) 40 | return false; 41 | if (!(obj instanceof Action)) 42 | return false; 43 | Action other = (Action) obj; 44 | if (type == null) { 45 | if (other.type != null) 46 | return false; 47 | } else if (!type.equals(other.type)) 48 | return false; 49 | return true; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "[" + type + "]"; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/InitStage.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule; 2 | 3 | import java.util.List; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.action.AutoActionType; 7 | import com.github.blovemaple.mj.action.PlayerActionType; 8 | import com.github.blovemaple.mj.action.StageSwitchAction; 9 | import com.github.blovemaple.mj.game.GameContext; 10 | 11 | /** 12 | * 初始阶段。此阶段是开局的默认阶段,唯一的作用是跳转到策略提供的第一阶段。 13 | * 14 | * @author blovemaple 15 | */ 16 | public class InitStage implements GameStage { 17 | public static final String NAME = "INIT"; 18 | 19 | @Override 20 | public String getName() { 21 | return NAME; 22 | } 23 | 24 | @Override 25 | public List getPlayerActionTypes() { 26 | return List.of(); 27 | } 28 | 29 | @Override 30 | public List getAutoActionTypes() { 31 | return List.of(); 32 | } 33 | 34 | @Override 35 | public Action getPriorAction(GameContext context) { 36 | return new StageSwitchAction(context.getGameStrategy().getFirstStage().getName()); 37 | } 38 | 39 | @Override 40 | public Action getFinalAction(GameContext context) { 41 | return null; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/blovemaple/mj/local/bazbot/BazBotTest.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.bazbot; 2 | 3 | import java.util.Set; 4 | import static com.github.blovemaple.mj.object.TileSuit.*; 5 | import static com.github.blovemaple.mj.object.TileRank.NumberRank.*; 6 | import static com.github.blovemaple.mj.object.TileRank.ZiRank.*; 7 | 8 | import com.github.blovemaple.mj.object.Tile; 9 | import com.github.blovemaple.mj.object.TileType; 10 | 11 | @SuppressWarnings("unused") 12 | public class BazBotTest { 13 | public static void main(String[] args) { 14 | bazBotAliveTilesTest(); 15 | } 16 | 17 | private static void bazBotAliveTilesTest() { 18 | Set aliveTiles = Set.of(// 19 | Tile.of(TileType.of(WAN, YI), 0), // 20 | Tile.of(TileType.of(WAN, ER), 0), // 21 | Tile.of(TileType.of(WAN, SAN), 0), // 22 | Tile.of(TileType.of(WAN, SI), 0), // 23 | Tile.of(TileType.of(WAN, WU), 0), // 24 | Tile.of(TileType.of(WAN, QI), 0), // 25 | Tile.of(TileType.of(WAN, QI), 1) // 26 | ); 27 | BazBotAliveTiles at = BazBotAliveTiles.of(aliveTiles); 28 | at.tileTypesToWin(); 29 | System.out.println("Neighborhoods: "); 30 | at.neighborhoods().forEach(System.out::println); 31 | System.out.println("Tile type to win: "); 32 | at.tileTypesToWin().forEach(System.out::println); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/simple/DealingStage.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.simple; 2 | 3 | import java.util.List; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.action.AutoActionType; 7 | import com.github.blovemaple.mj.action.PlayerActionType; 8 | import com.github.blovemaple.mj.action.StageSwitchAction; 9 | import com.github.blovemaple.mj.action.standard.AutoActionTypes; 10 | import com.github.blovemaple.mj.game.GameContext; 11 | import com.github.blovemaple.mj.rule.GameStage; 12 | 13 | /** 14 | * 发牌阶段。 15 | * 16 | * @author blovemaple 17 | */ 18 | public class DealingStage implements GameStage { 19 | public static final String NAME = "DEALING"; 20 | 21 | @Override 22 | public String getName() { 23 | return NAME; 24 | } 25 | 26 | @Override 27 | public List getPlayerActionTypes() { 28 | return List.of(); 29 | } 30 | 31 | @Override 32 | public List getAutoActionTypes() { 33 | return List.of(AutoActionTypes.DEAL); 34 | } 35 | 36 | @Override 37 | public Action getPriorAction(GameContext context) { 38 | return null; 39 | } 40 | 41 | @Override 42 | public Action getFinalAction(GameContext context) { 43 | return new StageSwitchAction(BeforePlayingStage.NAME); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/simple/BeforePlayingStage.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.simple; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | 5 | import java.util.List; 6 | 7 | import com.github.blovemaple.mj.action.Action; 8 | import com.github.blovemaple.mj.action.AutoActionType; 9 | import com.github.blovemaple.mj.action.PlayerActionType; 10 | import com.github.blovemaple.mj.action.StageSwitchAction; 11 | import com.github.blovemaple.mj.game.GameContext; 12 | import com.github.blovemaple.mj.rule.GameStage; 13 | 14 | /** 15 | * 开始打牌前的阶段,玩家在此阶段进行补花等动作。 16 | * 17 | * @author blovemaple 18 | */ 19 | public class BeforePlayingStage implements GameStage { 20 | public static final String NAME = "BEFORE_PLAYING"; 21 | 22 | @Override 23 | public String getName() { 24 | return NAME; 25 | } 26 | 27 | @Override 28 | public List getPlayerActionTypes() { 29 | return List.of(BUHUA, DRAW_BOTTOM); 30 | } 31 | 32 | @Override 33 | public List getAutoActionTypes() { 34 | return List.of(); 35 | } 36 | 37 | @Override 38 | public Action getPriorAction(GameContext context) { 39 | return null; 40 | } 41 | 42 | @Override 43 | public Action getFinalAction(GameContext context) { 44 | return new StageSwitchAction(PlayingStage.NAME); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/game/GameContext.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.game; 2 | 3 | import java.util.List; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.object.MahjongTable; 7 | import com.github.blovemaple.mj.object.PlayerInfo; 8 | import com.github.blovemaple.mj.object.PlayerLocation; 9 | import com.github.blovemaple.mj.rule.GameStage; 10 | import com.github.blovemaple.mj.rule.GameStrategy; 11 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 12 | 13 | /** 14 | * 一局游戏进行中的上下文信息。 15 | * 16 | * @author blovemaple 17 | */ 18 | public interface GameContext { 19 | 20 | public MahjongTable getTable(); 21 | 22 | public GameStrategy getGameStrategy(); 23 | 24 | public TimeLimitStrategy getTimeLimitStrategy(); 25 | 26 | public PlayerInfo getPlayerInfoByLocation(PlayerLocation location); 27 | 28 | public PlayerLocation getZhuangLocation(); 29 | 30 | public void setZhuangLocation(PlayerLocation zhuangLocation); 31 | 32 | public GameStage getStage(); 33 | 34 | public void setStage(GameStage stage); 35 | 36 | public void actionDone(Action action); 37 | 38 | public Action getLastAction(); 39 | 40 | public PlayerLocation getLastActionLocation(); 41 | 42 | public List getDoneActions(); 43 | 44 | public GameResult getGameResult(); 45 | 46 | public void setGameResult(GameResult gameResult); 47 | 48 | public GameContextPlayerView getPlayerView(PlayerLocation location); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/DrawBottomActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | 5 | import java.util.Set; 6 | import java.util.function.BiPredicate; 7 | import java.util.stream.Stream; 8 | 9 | import com.github.blovemaple.mj.action.Action; 10 | import com.github.blovemaple.mj.action.PlayerAction; 11 | import com.github.blovemaple.mj.game.GameContext; 12 | import com.github.blovemaple.mj.object.PlayerLocation; 13 | import com.github.blovemaple.mj.object.Tile; 14 | 15 | /** 16 | * 动作类型“摸底牌”。与摸牌动作的区别是,前提条件为自己补花或杠之后,并且是从牌墙底部摸。 17 | * 18 | * @author blovemaple 19 | */ 20 | public class DrawBottomActionType extends DrawActionType { 21 | 22 | protected DrawBottomActionType() { 23 | } 24 | 25 | @Override 26 | protected BiPredicate getLastActionPrecondition() { 27 | // 必须是自己补花或杠之后 28 | return (a, location) -> a instanceof PlayerAction && ((PlayerAction) a).getLocation() == location 29 | && Stream.of(BUHUA, ANGANG, ZHIGANG, BUGANG).anyMatch(type -> type.matchBy(a.getType())); 30 | } 31 | 32 | @Override 33 | protected void doLegalAction(GameContext context, PlayerLocation location, Set tiles) { 34 | Tile tile = context.getTable().drawBottom(1).get(0); 35 | context.getPlayerInfoByLocation(location).getAliveTiles().add(tile); 36 | context.getPlayerInfoByLocation(location).setLastDrawedTile(tile); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/StageSwitchActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import com.github.blovemaple.mj.action.Action; 4 | import com.github.blovemaple.mj.action.ActionType; 5 | import com.github.blovemaple.mj.action.IllegalActionException; 6 | import com.github.blovemaple.mj.action.StageSwitchAction; 7 | import com.github.blovemaple.mj.game.GameContext; 8 | import com.github.blovemaple.mj.rule.GameStage; 9 | 10 | /** 11 | * 状态切换的动作类型。 12 | * 13 | * @author blovemaple 14 | */ 15 | public class StageSwitchActionType implements ActionType { 16 | public static final StageSwitchActionType INSTANCE = new StageSwitchActionType(); 17 | 18 | @Override 19 | public String name() { 20 | return "STAGE"; 21 | } 22 | 23 | @Override 24 | public boolean isLegalAction(GameContext context, Action action) { 25 | if (!(action instanceof StageSwitchAction)) 26 | return false; 27 | return context.getGameStrategy().getStageByName(((StageSwitchAction) action).getNextStageName()) != null; 28 | } 29 | 30 | @Override 31 | public void doAction(GameContext context, Action action) throws IllegalActionException { 32 | GameStage nextStage = context.getGameStrategy().getStageByName(((StageSwitchAction) action).getNextStageName()); 33 | if (nextStage == null) 34 | throw new IllegalActionException(context, action); 35 | context.setStage(nextStage); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return name(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/AutoActionTypes.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import com.github.blovemaple.mj.action.Action; 4 | import com.github.blovemaple.mj.action.ActionType; 5 | import com.github.blovemaple.mj.action.AutoActionType; 6 | import com.github.blovemaple.mj.action.IllegalActionException; 7 | import com.github.blovemaple.mj.game.GameContext; 8 | 9 | /** 10 | * 自动动作类型枚举。
11 | * 枚举的每种动作类型包含对应Type类的单例,并委托调用其对应的方法。 12 | * 13 | * @author blovemaple 14 | */ 15 | public enum AutoActionTypes implements AutoActionType { 16 | /** 17 | * 发牌 18 | */ 19 | DEAL(new DealActionType()), 20 | /** 21 | * 流局 22 | */ 23 | LIUJU(new LiujuActionType()); 24 | 25 | private final ActionType type; 26 | 27 | private AutoActionTypes(ActionType type) { 28 | this.type = type; 29 | } 30 | 31 | // 以下都是委托方法,调用type的对应方法 32 | 33 | @Override 34 | public void doAction(GameContext context, Action action) throws IllegalActionException { 35 | type.doAction(context, action); 36 | } 37 | 38 | @Override 39 | public boolean matchBy(ActionType testType) { 40 | return type.matchBy(testType); 41 | } 42 | 43 | @Override 44 | public Class getRealTypeClass() { 45 | return type.getRealTypeClass(); 46 | } 47 | 48 | @Override 49 | public ActionType getRealTypeObject() { 50 | return type; 51 | } 52 | 53 | @Override 54 | public boolean isLegalAction(GameContext context, Action action) { 55 | return type.isLegalAction(context, action); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/TestBot.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | 5 | import java.util.Set; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import com.github.blovemaple.mj.action.Action; 9 | import com.github.blovemaple.mj.action.PlayerAction; 10 | import com.github.blovemaple.mj.action.PlayerActionType; 11 | import com.github.blovemaple.mj.game.GameContextPlayerView; 12 | import com.github.blovemaple.mj.object.Player; 13 | 14 | /** 15 | * 测试用的机器人,无脑摸牌无脑出牌(出牌之前假装想一会儿),别的不干。 16 | * 17 | * @author blovemaple 18 | */ 19 | public class TestBot implements Player { 20 | 21 | @Override 22 | public String getName() { 23 | return "TestBot"; 24 | } 25 | 26 | @Override 27 | public PlayerAction chooseAction(GameContextPlayerView contextView, 28 | Set actionTypes, 29 | PlayerAction illegalAction) throws InterruptedException { 30 | if (actionTypes.contains(DISCARD)) { 31 | TimeUnit.SECONDS.sleep(1); 32 | return new PlayerAction(contextView.getMyLocation(), DISCARD, 33 | contextView.getMyInfo().getAliveTiles().iterator().next()); 34 | } 35 | if (actionTypes.contains(DRAW)) 36 | return new PlayerAction(contextView.getMyLocation(), DRAW); 37 | return null; 38 | } 39 | 40 | @Override 41 | public void actionDone(GameContextPlayerView contextView, Action action) { 42 | } 43 | 44 | @Override 45 | public void timeLimit(GameContextPlayerView contextView, Integer secondsToGo) { 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileSuit.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | import com.github.blovemaple.mj.object.TileRank.HuaRank; 8 | import com.github.blovemaple.mj.object.TileRank.NumberRank; 9 | import com.github.blovemaple.mj.object.TileRank.ZiRank; 10 | 11 | /** 12 | * 牌的“花色”,即万、条、饼等。 13 | * 14 | * @author blovemaple 15 | */ 16 | public enum TileSuit { 17 | WAN(NumberRank.class, 4), TIAO(NumberRank.class, 4), BING(NumberRank.class, 18 | 4), ZI(ZiRank.class, 4), HUA(HuaRank.class, 1); 19 | 20 | private final Class> rankClass; 21 | private final int tileCountByType; 22 | 23 | private TileSuit(Class> rankClass, 24 | int tileCountByType) { 25 | this.rankClass = rankClass; 26 | this.tileCountByType = tileCountByType; 27 | } 28 | 29 | public Class> getRankClass() { 30 | return rankClass; 31 | } 32 | 33 | /** 34 | * 此种花色的每个牌型的牌的个数。 35 | */ 36 | public int getTileCountByType() { 37 | return tileCountByType; 38 | } 39 | 40 | public List> getAllRanks() { 41 | try { 42 | return Arrays.asList( 43 | (TileRank[]) rankClass.getMethod("values").invoke(null)); 44 | } catch (IllegalAccessException | IllegalArgumentException 45 | | InvocationTargetException | NoSuchMethodException 46 | | SecurityException e) { 47 | throw new RuntimeException( 48 | "Error invoke values method of rankType: " + rankClass); 49 | } 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/simple/SimpleGameStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.simple; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import com.github.blovemaple.mj.game.GameContext; 8 | import com.github.blovemaple.mj.object.PlayerLocation; 9 | import com.github.blovemaple.mj.rule.AbstractGameStrategy; 10 | import com.github.blovemaple.mj.rule.GameStage; 11 | import com.github.blovemaple.mj.rule.win.FanType; 12 | import com.github.blovemaple.mj.rule.win.WinType; 13 | 14 | /** 15 | * 简单游戏规则。固定坐庄、没有和牌限制、和牌固定为1番。 16 | * 17 | * @author blovemaple 18 | */ 19 | public class SimpleGameStrategy extends AbstractGameStrategy { 20 | 21 | @Override 22 | protected PlayerLocation nextZhuangLocation(GameContext context) { 23 | return PlayerLocation.EAST; 24 | } 25 | 26 | private static final List WIN_TYPES = Collections.singletonList(NormalWinType.get()); 27 | 28 | @Override 29 | public List getAllWinTypes() { 30 | return WIN_TYPES; 31 | } 32 | 33 | private static final List FAN_TYPES = Collections.singletonList(new SimpleFanType()); 34 | 35 | @Override 36 | public List getAllFanTypes() { 37 | return FAN_TYPES; 38 | } 39 | 40 | private List stages = List.of(new DealingStage(), new BeforePlayingStage(), new PlayingStage(), 41 | new FinishedStage()); 42 | 43 | @Override 44 | protected Collection getAllStages() { 45 | return stages; 46 | } 47 | 48 | @Override 49 | public GameStage getFirstStage() { 50 | return stages.get(0); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/simple/PlayingStage.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.simple; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | 5 | import java.util.List; 6 | 7 | import com.github.blovemaple.mj.action.Action; 8 | import com.github.blovemaple.mj.action.AutoActionType; 9 | import com.github.blovemaple.mj.action.PlayerActionType; 10 | import com.github.blovemaple.mj.action.StageSwitchAction; 11 | import com.github.blovemaple.mj.action.standard.AutoActionTypes; 12 | import com.github.blovemaple.mj.action.standard.PlayerActionTypes; 13 | import com.github.blovemaple.mj.game.GameContext; 14 | import com.github.blovemaple.mj.rule.GameStage; 15 | 16 | /** 17 | * 打牌中的阶段。 18 | * 19 | * @author blovemaple 20 | */ 21 | public class PlayingStage implements GameStage { 22 | public static final String NAME = "PLAYING"; 23 | 24 | @Override 25 | public String getName() { 26 | return NAME; 27 | } 28 | 29 | @Override 30 | public List getPlayerActionTypes() { 31 | return List.of(PlayerActionTypes.values()); 32 | } 33 | 34 | @Override 35 | public List getAutoActionTypes() { 36 | return List.of(); 37 | } 38 | 39 | @Override 40 | public Action getPriorAction(GameContext context) { 41 | if (context.getDoneActions().stream().map(Action::getType).anyMatch(type -> type == WIN)) 42 | return new StageSwitchAction(FinishedStage.NAME); 43 | return null; 44 | } 45 | 46 | @Override 47 | public Action getFinalAction(GameContext context) { 48 | return new Action(AutoActionTypes.LIUJU); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/DealActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import java.util.stream.Stream; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.action.AutoActionType; 7 | import com.github.blovemaple.mj.action.IllegalActionException; 8 | import com.github.blovemaple.mj.game.GameContext; 9 | import com.github.blovemaple.mj.object.MahjongTable; 10 | import com.github.blovemaple.mj.object.PlayerInfo; 11 | import com.github.blovemaple.mj.object.PlayerLocation; 12 | import com.github.blovemaple.mj.object.PlayerLocation.Relation; 13 | 14 | /** 15 | * 动作类型“发牌”。 16 | * 17 | * @author blovemaple 18 | */ 19 | public class DealActionType implements AutoActionType { 20 | 21 | protected DealActionType() { 22 | } 23 | 24 | @Override 25 | public boolean isLegalAction(GameContext context, Action action) { 26 | return context.getDoneActions().stream().map(Action::getType).noneMatch(this::matchBy); 27 | } 28 | 29 | @Override 30 | public void doAction(GameContext context, Action action) throws IllegalActionException { 31 | MahjongTable table = context.getTable(); 32 | PlayerLocation zhuang = context.getZhuangLocation(); 33 | for (int i = 0; i < 4; i++) { 34 | int drawCount = i < 3 ? 4 : 1; 35 | Stream.of(Relation.values()).map(zhuang::getLocationOf) 36 | .map(context::getPlayerInfoByLocation) 37 | .map(PlayerInfo::getAliveTiles) 38 | .forEach(aliveTiles -> aliveTiles 39 | .addAll(table.draw(drawCount))); 40 | } 41 | context.getPlayerInfoByLocation(zhuang).getAliveTiles() 42 | .addAll(table.draw(1)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileRank.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | /** 4 | * 牌的花色下面的小种类。如万牌中的“一”、“二”,风牌中的“东”、“南”等。
5 | * 实现类必须有values()方法返回相应的种类数组(枚举类就可以)。因为TileSuit会用values()方法获取对应的种类。 6 | * 7 | * @author blovemaple 8 | */ 9 | public interface TileRank> extends Comparable { 10 | /** 11 | * 返回名称。用于唯一标识。 12 | */ 13 | public String name(); 14 | 15 | /** 16 | * 万、条、饼等花色使用的数字种类。 17 | */ 18 | public enum NumberRank implements TileRank { 19 | // 顺序勿动!ofNumber依赖顺序 20 | YI(1), ER(2), SAN(3), SI(4), WU(5), LIU(6), QI(7), BA(8), JIU(9); 21 | 22 | private final int number; 23 | 24 | private NumberRank(int number) { 25 | this.number = number; 26 | } 27 | 28 | public int number() { 29 | return number; 30 | } 31 | 32 | public static NumberRank ofNumber(int number) { 33 | return values()[number - 1]; 34 | } 35 | } 36 | 37 | /** 38 | * 字牌的种类。 39 | */ 40 | public enum ZiRank implements TileRank { 41 | DONG_FENG, NAN, XI, BEI, ZHONG, FA, BAI 42 | } 43 | 44 | /** 45 | * 花牌的种类。 46 | */ 47 | public enum HuaRank implements TileRank { 48 | CHUN, XIA, QIU, DONG_HUA, MEI, LAN, ZHU, JU 49 | } 50 | 51 | @SuppressWarnings({ "rawtypes", "unchecked" }) 52 | public static int compare(TileRank r1, TileRank r2) { 53 | if (r1.getClass() != r2.getClass()) { 54 | Integer.compare(r1.getClass().hashCode(), r2.getClass().hashCode()); 55 | } 56 | return compare0((TileRank) r1, (TileRank) r2); 57 | } 58 | 59 | private static > int compare0(T r1, T r2) { 60 | return r1.compareTo(r2); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/game/GameContextPlayerView.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.game; 2 | 3 | import java.util.List; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.object.MahjongTablePlayerView; 7 | import com.github.blovemaple.mj.object.PlayerInfo; 8 | import com.github.blovemaple.mj.object.PlayerLocation; 9 | import com.github.blovemaple.mj.object.Tile; 10 | import com.github.blovemaple.mj.rule.GameStrategy; 11 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 12 | 13 | /** 14 | * {@link GameContext}给一个玩家的视图接口。 15 | * 16 | * @author blovemaple 17 | */ 18 | public interface GameContextPlayerView { 19 | 20 | /** 21 | * 返回麻将桌的玩家视图。 22 | */ 23 | public MahjongTablePlayerView getTableView(); 24 | 25 | /** 26 | * 返回游戏策略。 27 | */ 28 | public GameStrategy getGameStrategy(); 29 | 30 | /** 31 | * 返回限时策略。 32 | */ 33 | public TimeLimitStrategy getTimeLimitStrategy(); 34 | 35 | /** 36 | * 返回当前玩家位置。 37 | */ 38 | public PlayerLocation getMyLocation(); 39 | 40 | /** 41 | * 返回当前玩家信息。 42 | */ 43 | public PlayerInfo getMyInfo(); 44 | 45 | /** 46 | * 返回庄家位置。 47 | */ 48 | public PlayerLocation getZhuangLocation(); 49 | 50 | /** 51 | * 返回当前阶段名称。 52 | */ 53 | public String getStageName(); 54 | 55 | /** 56 | * 返回到目前为止做出的最后一个动作。 57 | */ 58 | public Action getLastAction(); 59 | 60 | /** 61 | * 返回到目前为止做出的最后一个动作的玩家位置。 62 | */ 63 | public PlayerLocation getLastActionLocation(); 64 | 65 | /** 66 | * 如果刚刚摸牌,则返回刚摸的牌,否则返回null。 67 | */ 68 | public Tile getJustDrawedTile(); 69 | 70 | /** 71 | * 返回已经做完的动作。 72 | */ 73 | public List getDoneActions(); 74 | 75 | /** 76 | * 如果已结束则返回游戏结果,否则返回null。 77 | */ 78 | public GameResult getGameResult(); 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/AngangActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.object.TileGroupType.*; 4 | 5 | import java.util.Set; 6 | import java.util.function.Predicate; 7 | 8 | import com.github.blovemaple.mj.action.AbstractPlayerActionType; 9 | import com.github.blovemaple.mj.game.GameContext; 10 | import com.github.blovemaple.mj.game.GameContextPlayerView; 11 | import com.github.blovemaple.mj.object.PlayerInfo; 12 | import com.github.blovemaple.mj.object.PlayerLocation; 13 | import com.github.blovemaple.mj.object.Tile; 14 | import com.github.blovemaple.mj.object.TileGroup; 15 | 16 | /** 17 | * 动作类型“暗杠”。 18 | * 19 | * @author blovemaple 20 | */ 21 | public class AngangActionType extends AbstractPlayerActionType { 22 | 23 | protected AngangActionType() { 24 | } 25 | 26 | @Override 27 | public boolean canPass(GameContext context, PlayerLocation location) { 28 | return true; 29 | } 30 | 31 | @Override 32 | protected Predicate getAliveTileSizePrecondition() { 33 | return size -> size % 3 == 2; 34 | } 35 | 36 | @Override 37 | protected int getActionTilesSize() { 38 | return ANGANG_GROUP.size(); 39 | } 40 | 41 | @Override 42 | protected boolean isLegalActionWithPreconition(GameContextPlayerView context, 43 | Set tiles) { 44 | return ANGANG_GROUP.isLegalTiles(tiles); 45 | } 46 | 47 | @Override 48 | protected void doLegalAction(GameContext context, PlayerLocation location, 49 | Set tiles) { 50 | PlayerInfo playerInfo = context.getPlayerInfoByLocation(location); 51 | playerInfo.getAliveTiles().removeAll(tiles); 52 | playerInfo.getTileGroups().add(new TileGroup(ANGANG_GROUP, tiles)); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/DiscardActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import java.util.Set; 4 | import java.util.function.Predicate; 5 | 6 | import com.github.blovemaple.mj.action.AbstractPlayerActionType; 7 | import com.github.blovemaple.mj.game.GameContext; 8 | import com.github.blovemaple.mj.game.GameContextPlayerView; 9 | import com.github.blovemaple.mj.object.PlayerInfo; 10 | import com.github.blovemaple.mj.object.PlayerLocation; 11 | import com.github.blovemaple.mj.object.Tile; 12 | 13 | /** 14 | * 动作类型“打牌”。 15 | * 16 | * @author blovemaple 17 | */ 18 | public class DiscardActionType extends AbstractPlayerActionType { 19 | 20 | protected DiscardActionType() { 21 | } 22 | 23 | @Override 24 | public boolean canPass(GameContext context, PlayerLocation location) { 25 | return false; 26 | } 27 | 28 | @Override 29 | protected Predicate getAliveTileSizePrecondition() { 30 | return size -> size % 3 == 2; 31 | } 32 | 33 | @Override 34 | protected int getActionTilesSize() { 35 | return 1; 36 | } 37 | 38 | @Override 39 | protected boolean isLegalActionWithPreconition(GameContextPlayerView context, Set tiles) { 40 | if (!context.getMyInfo().isTing()) { 41 | // 没听牌时,所有aliveTiles都可以打出 42 | return true; 43 | } else { 44 | // 听牌后只允许打出最后摸的牌 45 | Tile justDrawed = context.getJustDrawedTile(); 46 | return justDrawed != null 47 | && justDrawed.equals(tiles.iterator().next()); 48 | } 49 | } 50 | 51 | @Override 52 | protected void doLegalAction(GameContext context, PlayerLocation location, Set tiles) { 53 | PlayerInfo playerInfo = context.getPlayerInfoByLocation(location); 54 | playerInfo.getAliveTiles().removeAll(tiles); 55 | playerInfo.getDiscardedTiles().addAll(tiles); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/bazbot/BazBot.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.bazbot; 2 | 3 | import static java.util.Comparator.*; 4 | import static java.util.stream.Collectors.*; 5 | 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.logging.Logger; 9 | 10 | import org.apache.commons.lang3.tuple.Pair; 11 | 12 | import com.github.blovemaple.mj.action.PlayerAction; 13 | import com.github.blovemaple.mj.action.PlayerActionType; 14 | import com.github.blovemaple.mj.game.GameContextPlayerView; 15 | import com.github.blovemaple.mj.local.AbstractBot; 16 | 17 | /** 18 | * @author blovemaple 19 | */ 20 | public class BazBot extends AbstractBot { 21 | private static final Logger logger = Logger.getLogger(BazBot.class.getSimpleName()); 22 | 23 | public BazBot(String name) { 24 | super(name); 25 | } 26 | 27 | public BazBot() { 28 | this("BazBot"); 29 | } 30 | 31 | @Override 32 | protected PlayerAction chooseCpgdAction(GameContextPlayerView contextView, Set actionTypes, 33 | List actions) throws InterruptedException { 34 | BazBotSimContext simContext = new BazBotSimContext(contextView); 35 | List> actionAndScores = actions.stream() 36 | // 并行 37 | .parallel() 38 | // 模拟动作并计算评分(不能直接用max否则会重复计算) 39 | .map(action -> Pair.of(action, simContext.afterSimAction(action).score())) 40 | // 按评分从高到底排序方便看日志 41 | .sorted(comparing(actionAndScore -> -actionAndScore.getRight())).collect(toList()); 42 | 43 | actionAndScores.forEach(actionAndScore -> logger.info( 44 | () -> "BOT Action candidate " + actionAndScore.getLeft() + " score " + actionAndScore.getRight())); 45 | 46 | // 选评分最高的一个 47 | PlayerAction chosenAction = actionAndScores.get(0).getKey(); 48 | return chosenAction; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/ActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | import com.github.blovemaple.mj.action.standard.CpgActionType; 4 | import com.github.blovemaple.mj.action.standard.PlayerActionTypes; 5 | import com.github.blovemaple.mj.game.GameContext; 6 | 7 | /** 8 | * 做出动作的类型。 9 | * 10 | * @author blovemaple 11 | */ 12 | public interface ActionType { 13 | 14 | /** 15 | * 名称,用作唯一标识。
16 | * 默认实现为{@code this.getClass().getSimpleName()}。
17 | * 注意:使用{@link PlayerActionTypes}时,name为枚举值的{@code name(})。 18 | * 19 | * @see com.github.blovemaple.mj.action.ActionType#name() 20 | */ 21 | public default String name() { 22 | return this.getClass().getSimpleName(); 23 | } 24 | 25 | /** 26 | * 判断指定动作是否合法。 27 | */ 28 | public boolean isLegalAction(GameContext context, Action action); 29 | 30 | /** 31 | * 执行指定动作。 32 | * 33 | * @throws IllegalActionException 34 | * 动作非法 35 | */ 36 | public void doAction(GameContext context, Action action) throws IllegalActionException; 37 | 38 | /** 39 | * 返回指定的动作类型是否是此类表示的动作。
40 | * 有些动作有上下从属关系,比如“摸底牌”是“摸牌”,但反过来不是。
41 | * 默认实现为判断真正类的从属关系。如果不是这样(例如{@link PlayerActionTypes}和{@link CpgActionType} 42 | * )则需要重写此方法。 43 | */ 44 | public default boolean matchBy(ActionType testType) { 45 | return getRealTypeClass().isAssignableFrom(testType.getRealTypeClass()); 46 | } 47 | 48 | /** 49 | * 返回真正的动作类型类。
50 | * 默认实现为返回此对象的类。如果不是这样(例如{@link PlayerActionTypes})则需要重写此方法。 51 | */ 52 | public default Class getRealTypeClass() { 53 | return this.getClass(); 54 | } 55 | 56 | /** 57 | * 返回真正的动作类型对象。
58 | * 默认实现为返回此对象。如果不是这样(例如{@link PlayerActionTypes})则需要重写此方法。 59 | */ 60 | public default ActionType getRealTypeObject() { 61 | return this; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/LocalGame.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local; 2 | 3 | import static com.github.blovemaple.mj.object.PlayerLocation.*; 4 | 5 | import java.util.function.Supplier; 6 | 7 | import com.github.blovemaple.mj.game.MahjongGame; 8 | import com.github.blovemaple.mj.local.bazbot.BazBot; 9 | import com.github.blovemaple.mj.object.MahjongTable; 10 | import com.github.blovemaple.mj.object.Player; 11 | import com.github.blovemaple.mj.rule.GameStrategy; 12 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 13 | import com.github.blovemaple.mj.rule.simple.SimpleGameStrategy; 14 | 15 | /** 16 | * 本地游戏。 17 | * 18 | * @author blovemaple 19 | */ 20 | public class LocalGame { 21 | private GameStrategy gameStrategy = new SimpleGameStrategy(); 22 | private TimeLimitStrategy timeStrategy = TimeLimitStrategy.NO_LIMIT; 23 | 24 | private Supplier botSupplier = () -> new BazBot().thinkingTime(1000, 3000); 25 | 26 | private Player localPlayer; 27 | private Supplier newGameChecker; 28 | 29 | /** 30 | * 新建一个实例。 31 | * 32 | * @param localPlayer 33 | * 本地玩家 34 | * @param newGameChecker 35 | * 一局结束后决定是否开始新的一局的函数 36 | */ 37 | public LocalGame(Player localPlayer, Supplier newGameChecker) { 38 | this.localPlayer = localPlayer; 39 | this.newGameChecker = newGameChecker; 40 | } 41 | 42 | public void play() throws InterruptedException { 43 | MahjongTable table = new MahjongTable(); 44 | table.init(); 45 | table.setPlayer(EAST, localPlayer); 46 | table.setPlayer(SOUTH, botSupplier.get()); 47 | table.setPlayer(WEST, botSupplier.get()); 48 | table.setPlayer(NORTH, botSupplier.get()); 49 | 50 | MahjongGame game = new MahjongGame(gameStrategy, timeStrategy); 51 | while (newGameChecker.get()) { 52 | game.play(table); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/github/blovemaple/mj/local/foobot/SimpleGameStrategyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.foobot; 2 | 3 | import static com.github.blovemaple.mj.object.TileRank.NumberRank.*; 4 | import static com.github.blovemaple.mj.object.TileSuit.*; 5 | 6 | import org.junit.After; 7 | import org.junit.AfterClass; 8 | import org.junit.Before; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | 12 | import com.github.blovemaple.mj.object.PlayerInfo; 13 | import com.github.blovemaple.mj.object.Tile; 14 | import com.github.blovemaple.mj.object.TileType; 15 | import com.github.blovemaple.mj.rule.simple.SimpleGameStrategy; 16 | 17 | public class SimpleGameStrategyTest { 18 | @SuppressWarnings("unused") 19 | private SimpleGameStrategy strategy = new SimpleGameStrategy(); 20 | private PlayerInfo selfInfo; 21 | 22 | @BeforeClass 23 | public static void setUpBeforeClass() throws Exception { 24 | } 25 | 26 | @AfterClass 27 | public static void tearDownAfterClass() throws Exception { 28 | } 29 | 30 | @Before 31 | public void setUp() throws Exception { 32 | selfInfo = new PlayerInfo(); 33 | 34 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, YI), 0)); 35 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, YI), 1)); 36 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, ER), 0)); 37 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, SAN), 0)); 38 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, SI), 0)); 39 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, YI), 0)); 40 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, YI), 1)); 41 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, YI), 2)); 42 | } 43 | 44 | @After 45 | public void tearDown() throws Exception { 46 | } 47 | 48 | @Test 49 | public void test() { 50 | // strategy.getFans(selfInfo, null).forEach((a, b) -> System.out.println(a + " " + b)); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/Tile.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.io.Serializable; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.IntStream; 9 | 10 | /** 11 | * 麻将牌。 12 | * 13 | * @author blovemaple 14 | */ 15 | public class Tile implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | private static final List all; 19 | static { 20 | // 初始化所有牌 21 | List allTiles = TileType.all().stream() 22 | .flatMap(type -> IntStream 23 | .range(0, type.suit().getTileCountByType()) 24 | .mapToObj(id -> new Tile(type, id))) 25 | .collect(Collectors.toList()); 26 | all = Collections.unmodifiableList(allTiles); 27 | } 28 | 29 | /** 30 | * 返回所有144张牌的列表。 31 | */ 32 | public static List all() { 33 | return all; 34 | } 35 | 36 | /** 37 | * 返回指定牌型的所有牌的集合。 38 | */ 39 | public static Set allOfType(TileType type) { 40 | return all.stream().filter(tile -> tile.type() == type) 41 | .collect(Collectors.toSet()); 42 | } 43 | 44 | /** 45 | * 返回指定牌。 46 | */ 47 | public static Tile of(TileType type, int id) { 48 | return all.stream() 49 | .filter(tile -> tile.type() == type && tile.id() == id) 50 | .findAny().orElse(null); 51 | } 52 | 53 | private final TileType type; 54 | private final int id;// 每个牌型从0到3。 55 | 56 | private Tile(TileType type, int id) { 57 | this.type = type; 58 | this.id = id; 59 | } 60 | 61 | /** 62 | * 返回牌型。 63 | */ 64 | public TileType type() { 65 | return type; 66 | } 67 | 68 | /** 69 | * 返回ID,同一牌型从0开始,通常是[0,3]。 70 | */ 71 | public int id() { 72 | return id; 73 | } 74 | 75 | /** 76 | * Just for debug. 77 | * 78 | * @see java.lang.Object#toString() 79 | */ 80 | @Override 81 | public String toString() { 82 | return "[" + type + ", " + id + "]"; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileUnit.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import static com.github.blovemaple.mj.object.TileUnit.TileUnitSource.*; 4 | 5 | import java.util.Collection; 6 | import java.util.Comparator; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | /** 11 | * 牌的单元,即判断和牌时对牌的分组,如顺子、刻子、杠子、将牌等。 12 | * 13 | * @author blovemaple 14 | */ 15 | public class TileUnit { 16 | private final TileUnitType type; 17 | private final Set tiles; 18 | private TileType firstTileType; // 最小的一个牌型 19 | private final TileUnitSource source; 20 | private final Tile gotTile; 21 | 22 | public enum TileUnitSource { 23 | SELF, GOT 24 | } 25 | 26 | public static TileUnit self(TileUnitType type, Collection tiles) { 27 | return new TileUnit(type, tiles, SELF, null); 28 | } 29 | 30 | public static TileUnit got(TileUnitType type, Collection tiles, Tile gotTile) { 31 | return new TileUnit(type, tiles, GOT, gotTile); 32 | } 33 | 34 | public TileUnit(TileUnitType type, Collection tiles, TileUnitSource source, Tile gotTile) { 35 | this.type = type; 36 | this.tiles = tiles instanceof Set ? (Set) tiles : new HashSet<>(tiles); 37 | this.source = source; 38 | this.gotTile = gotTile; 39 | } 40 | 41 | public TileUnitType getType() { 42 | return type; 43 | } 44 | 45 | public Set getTiles() { 46 | return tiles; 47 | } 48 | 49 | public TileType getFirstTileType() { 50 | if (firstTileType == null) 51 | firstTileType = tiles.stream().map(Tile::type).min(Comparator.naturalOrder()).orElse(null); 52 | return firstTileType; 53 | } 54 | 55 | public TileUnitSource getSource() { 56 | return source; 57 | } 58 | 59 | public Tile getGotTile() { 60 | return gotTile; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return "TileUnit [type=" + type + ", tiles=" + tiles + "]"; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/DrawActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | import static com.github.blovemaple.mj.object.PlayerLocation.Relation.*; 5 | 6 | import java.util.Set; 7 | import java.util.function.BiPredicate; 8 | 9 | import com.github.blovemaple.mj.action.AbstractPlayerActionType; 10 | import com.github.blovemaple.mj.action.Action; 11 | import com.github.blovemaple.mj.action.PlayerAction; 12 | import com.github.blovemaple.mj.game.GameContext; 13 | import com.github.blovemaple.mj.game.GameContextPlayerView; 14 | import com.github.blovemaple.mj.object.PlayerLocation; 15 | import com.github.blovemaple.mj.object.Tile; 16 | 17 | /** 18 | * 动作类型“摸牌”。 19 | * 20 | * @author blovemaple 21 | */ 22 | public class DrawActionType extends AbstractPlayerActionType { 23 | 24 | protected DrawActionType() { 25 | } 26 | 27 | @Override 28 | public boolean canPass(GameContext context, PlayerLocation location) { 29 | return false; 30 | } 31 | 32 | @Override 33 | protected BiPredicate getLastActionPrecondition() { 34 | // 必须是上家打牌后 35 | return (a, location) -> DISCARD.matchBy(a.getType()) 36 | && location.getRelationOf(((PlayerAction) a).getLocation()) == PREVIOUS; 37 | } 38 | 39 | @Override 40 | protected int getActionTilesSize() { 41 | return 0; 42 | } 43 | 44 | @Override 45 | protected boolean isLegalActionWithPreconition(GameContextPlayerView context, 46 | Set tiles) { 47 | // 牌墙中必须有牌才能摸 48 | return context.getTableView().getTileWallSize() > 0; 49 | } 50 | 51 | @Override 52 | protected void doLegalAction(GameContext context, PlayerLocation location, Set tiles) { 53 | Tile tile = context.getTable().draw(1).get(0); 54 | context.getPlayerInfoByLocation(location).getAliveTiles().add(tile); 55 | context.getPlayerInfoByLocation(location).setLastDrawedTile(tile); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/game/GameResult.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.game; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | 6 | import com.github.blovemaple.mj.object.PlayerInfo; 7 | import com.github.blovemaple.mj.object.PlayerLocation; 8 | import com.github.blovemaple.mj.object.Tile; 9 | import com.github.blovemaple.mj.rule.win.FanType; 10 | 11 | /** 12 | * 一局游戏的结果。winnerLocation为null表示流局。 13 | * 14 | * @author blovemaple 15 | */ 16 | public class GameResult implements Serializable { 17 | private static final long serialVersionUID = 5927092445320750860L; 18 | 19 | private final Map playerInfos; 20 | private final PlayerLocation zhuangLocation; 21 | 22 | private PlayerLocation winnerLocation; 23 | private Tile winTile; 24 | private PlayerLocation paoerLocation; 25 | private Map fans; 26 | 27 | public GameResult(Map playerInfos, 28 | PlayerLocation zhuangLocation) { 29 | this.playerInfos = playerInfos; 30 | this.zhuangLocation = zhuangLocation; 31 | } 32 | 33 | public PlayerLocation getWinnerLocation() { 34 | return winnerLocation; 35 | } 36 | 37 | public void setWinnerLocation(PlayerLocation winnerLocation) { 38 | this.winnerLocation = winnerLocation; 39 | } 40 | 41 | public Tile getWinTile() { 42 | return winTile; 43 | } 44 | 45 | public void setWinTile(Tile winTile) { 46 | this.winTile = winTile; 47 | } 48 | 49 | public PlayerLocation getPaoerLocation() { 50 | return paoerLocation; 51 | } 52 | 53 | public void setPaoerLocation(PlayerLocation paoerLocation) { 54 | this.paoerLocation = paoerLocation; 55 | } 56 | 57 | public Map getFans() { 58 | return fans; 59 | } 60 | 61 | public void setFans(Map fans) { 62 | this.fans = fans; 63 | } 64 | 65 | public Map getPlayerInfos() { 66 | return playerInfos; 67 | } 68 | 69 | public PlayerLocation getZhuangLocation() { 70 | return zhuangLocation; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/ActionTypeAndLocation.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | import java.util.Objects; 4 | 5 | import com.github.blovemaple.mj.game.GameContext; 6 | import com.github.blovemaple.mj.object.PlayerLocation; 7 | 8 | /** 9 | * 动作类型和玩家位置的组合。 10 | * 11 | * @author blovemaple 12 | */ 13 | public class ActionTypeAndLocation { 14 | private final ActionType actionType; 15 | private final PlayerLocation location; 16 | 17 | private final GameContext context; 18 | 19 | public ActionTypeAndLocation(ActionType actionType, 20 | PlayerLocation location) { 21 | this(actionType, location, null); 22 | } 23 | 24 | public ActionTypeAndLocation(ActionType actionType, PlayerLocation location, 25 | GameContext context) { 26 | Objects.requireNonNull(actionType); 27 | this.actionType = actionType; 28 | this.location = location; 29 | this.context = context; 30 | } 31 | 32 | public ActionType getActionType() { 33 | return actionType; 34 | } 35 | 36 | public PlayerLocation getLocation() { 37 | return location; 38 | } 39 | 40 | public GameContext getContext() { 41 | return context; 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | final int prime = 31; 47 | int result = 1; 48 | result = prime * result 49 | + ((actionType == null) ? 0 : actionType.hashCode()); 50 | result = prime * result 51 | + ((location == null) ? 0 : location.hashCode()); 52 | return result; 53 | } 54 | 55 | @Override 56 | public boolean equals(Object obj) { 57 | if (this == obj) 58 | return true; 59 | if (obj == null) 60 | return false; 61 | if (getClass() != obj.getClass()) 62 | return false; 63 | ActionTypeAndLocation other = (ActionTypeAndLocation) obj; 64 | if (actionType == null) { 65 | if (other.actionType != null) 66 | return false; 67 | } else if (!actionType.equals(other.actionType)) 68 | return false; 69 | if (location != other.location) 70 | return false; 71 | return true; 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/barbot/BarBot.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.barbot; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.Set; 6 | import java.util.concurrent.CancellationException; 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.ForkJoinPool; 9 | import java.util.concurrent.Future; 10 | 11 | import com.github.blovemaple.mj.action.PlayerAction; 12 | import com.github.blovemaple.mj.action.PlayerActionType; 13 | import com.github.blovemaple.mj.game.GameContextPlayerView; 14 | import com.github.blovemaple.mj.local.AbstractBot; 15 | 16 | /** 17 | * @author blovemaple 18 | */ 19 | @Deprecated 20 | public class BarBot extends AbstractBot { 21 | private String name; 22 | 23 | public BarBot(String name) { 24 | super(name); 25 | this.name = name; 26 | } 27 | 28 | public BarBot() { 29 | this("BarBot"); 30 | } 31 | 32 | @Override 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | private Future selectFuture; 38 | 39 | @Override 40 | protected PlayerAction chooseCpgdAction(GameContextPlayerView contextView, Set actionTypes, 41 | List actions) throws InterruptedException { 42 | if (selectFuture != null && !selectFuture.isDone()) 43 | throw new IllegalStateException("Another select task is active."); 44 | 45 | if (Collections.disjoint(actionTypes, BarBotCpgdSelectTask.ACTION_TYPES)) 46 | return null; 47 | 48 | Future futureResult = ForkJoinPool.commonPool() 49 | .submit(new BarBotCpgdSelectTask(contextView, actionTypes)); 50 | try { 51 | return futureResult.get(); 52 | } catch (InterruptedException e) { 53 | // 选择被game中断,不再继续选择了 54 | selectFuture.cancel(true); 55 | throw e; 56 | } catch (ExecutionException e) { 57 | // 选择过程出现错误 58 | throw new RuntimeException(e); 59 | } catch (CancellationException e) { 60 | // 应该不会在这出来 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/win/FanType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.win; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.HashSet; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | /** 11 | * 番种。 12 | * 13 | * @author blovemaple 14 | */ 15 | public interface FanType extends FanTypeMatcher { 16 | /** 17 | * 返回番种的名称,作为标识。 18 | */ 19 | public String name(); 20 | 21 | /** 22 | * 返回计入一次的番数。 23 | */ 24 | public int score(); 25 | 26 | /** 27 | * 返回被此番种覆盖的番种。如果此番种已计入,则不计被覆盖的番种。 28 | */ 29 | public Set covered(); 30 | 31 | /** 32 | * 算番。 33 | * 34 | * @param winInfo 35 | * 被检查的牌 36 | * @param fanTypes 37 | * 使用的番种 38 | * @param winTypes 39 | * 使用的和牌类型 40 | * @return 符合的番种和番数 41 | */ 42 | public static Map getFans(WinInfo winInfo, Collection fanTypes, 43 | Collection winTypes) { 44 | // 先parse和牌units 45 | winTypes.forEach(winType -> winType.parseWinTileUnits(winInfo)); 46 | // 如果没parse出来,说明不和牌,直接返回空map 47 | if (winInfo.getUnits() == null || winInfo.getUnits().isEmpty()) 48 | return Collections.emptyMap(); 49 | 50 | // 算番 51 | Map fans = new HashMap<>(); 52 | Set coveredFanTypes = new HashSet<>(); 53 | fanTypes.forEach(crtFanType -> { 54 | if (coveredFanTypes.contains(crtFanType)) 55 | return; 56 | 57 | Integer matchCount; 58 | 59 | matchCount = winInfo.getFans().get(crtFanType); 60 | if (matchCount == null) { 61 | matchCount = crtFanType.matchCount(winInfo); 62 | if (matchCount > 0) 63 | winInfo.getFans().put(crtFanType, matchCount); 64 | } 65 | 66 | if (matchCount > 0) { 67 | fans.put(crtFanType, crtFanType.score() * matchCount); 68 | Set covered = crtFanType.covered(); 69 | if (covered != null && !covered.isEmpty()) 70 | coveredFanTypes.addAll(covered); 71 | } 72 | }); 73 | 74 | return fans; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/resources/message.properties: -------------------------------------------------------------------------------- 1 | TILE_SUIT_WAN=\u842C 2 | TILE_SUIT_TIAO=\u6761 3 | TILE_SUIT_BING=\u997C 4 | TILE_SUIT_ZI=\u5B57 5 | TILE_SUIT_HUA=\u82B1 6 | 7 | TILE_RANK_YI=\u4E00 8 | TILE_RANK_ER=\u4E8C 9 | TILE_RANK_SAN=\u4E09 10 | TILE_RANK_SI=\u56DB 11 | TILE_RANK_WU=\u4E94 12 | TILE_RANK_LIU=\u516D 13 | TILE_RANK_QI=\u4E03 14 | TILE_RANK_BA=\u516B 15 | TILE_RANK_JIU=\u4E5D 16 | 17 | TILE_RANK_DONG_FENG=\u6771 18 | TILE_RANK_NAN=\u5357 19 | TILE_RANK_XI=\u897F 20 | TILE_RANK_BEI=\u5317 21 | TILE_RANK_ZHONG=\u4E2D 22 | TILE_RANK_FA=\u767C 23 | TILE_RANK_BAI=\u767D 24 | 25 | TILE_RANK_CHUN=\u6625 26 | TILE_RANK_XIA=\u590F 27 | TILE_RANK_QIU=\u79CB 28 | TILE_RANK_DONG_HUA=\u51AC 29 | TILE_RANK_MEI=\u6885 30 | TILE_RANK_LAN=\u5170 31 | TILE_RANK_ZHU=\u7AF9 32 | TILE_RANK_JU=\u83CA 33 | 34 | ACTION_TYPE_DEAL=\u53D1\u724C 35 | ACTION_TYPE_CHI=\u5403 36 | ACTION_TYPE_PENG=\u78B0 37 | ACTION_TYPE_ZHIGANG=\u6760 38 | ACTION_TYPE_BUGANG=\u8865\u6760 39 | ACTION_TYPE_ANGANG=\u6697\u6760 40 | ACTION_TYPE_BUHUA=\u8865\u82B1 41 | ACTION_TYPE_DISCARD=\u51FA 42 | ACTION_TYPE_DISCARD_WITH_TING=\u51FA(\u542C) 43 | ACTION_TYPE_DRAW=\u6478 44 | ACTION_TYPE_DRAW_BOTTOM=\u6478\u5E95 45 | ACTION_TYPE_WIN=\u548C 46 | ACTION_TYPE_LIUJU=\u6D41\u5C40 47 | 48 | PLAYER_LOCATION_EAST=\u4E1C 49 | PLAYER_LOCATION_SOUTH=\u5357 50 | PLAYER_LOCATION_WEST=\u897F 51 | PLAYER_LOCATION_NORTH=\u5317 52 | 53 | PLAYER_RELATION_SELF=\u672C\u5BB6 54 | PLAYER_RELATION_NEXT=\u4E0B\u5BB6 55 | PLAYER_RELATION_OPPOSITE=\u5BF9\u5BB6 56 | PLAYER_RELATION_PREVIOUS=\u4E0A\u5BB6 57 | 58 | FAN_TYPE_SIMPLE=\u548C 59 | 60 | DEAL_DONE=\u53D1\u724C\u7ED3\u675F 61 | ZHUANG=\u5E84\u5BB6 62 | TING=\u542C 63 | SPACE_KEY=\u7A7A\u683C 64 | M_KEY=M 65 | H_KEY=H 66 | SLASH_KEY=/ 67 | COMMA_AND_PERIOD_KEY=,. 68 | MOVE_CHOICE=\u79FB\u52A8 69 | PASS=\u653E\u5F03 70 | ZIMO=\u81EA\u6478 71 | DIANPAO=\u70B9\u70AE 72 | FAN_TOTLE=\u603B\u8BA1 73 | FAN=\u756A 74 | WINDOW_TOO_NARROW=\u7A97\u53E3\u592A\u7A84\u4E86\u2026 75 | NEW_GAME_QUESTION=\u5F00\u59CB\u4E00\u5C40\uFF1F(Y/N) 76 | MOVE_TIP=\u63D0\u793A\uFF1A\u6309 , . \u952E\u5DE6\u53F3\u79FB\u52A8\u9009\u62E9 77 | GITHUB_TIP=\u6E90\u7801\uFF1Ahttps://github.com/blovemaple/mahjong -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/DiscardWithTingActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.utils.MyUtils.*; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import com.github.blovemaple.mj.game.GameContext; 9 | import com.github.blovemaple.mj.game.GameContextPlayerView; 10 | import com.github.blovemaple.mj.object.PlayerInfo; 11 | import com.github.blovemaple.mj.object.PlayerLocation; 12 | import com.github.blovemaple.mj.object.Tile; 13 | import com.github.blovemaple.mj.rule.GameStrategy; 14 | import com.github.blovemaple.mj.rule.win.WinInfo; 15 | 16 | /** 17 | * 动作类型“打牌的同时听牌”。与打牌动作的区别是: 18 | *
  • 合法性要判断打出后是否可以听; 19 | *
  • 执行动作时要设置听牌状态。 20 | * 21 | * @author blovemaple 22 | */ 23 | public class DiscardWithTingActionType extends DiscardActionType { 24 | 25 | protected DiscardWithTingActionType() { 26 | } 27 | 28 | @Override 29 | protected boolean isAllowedInTing() { 30 | return false; 31 | } 32 | 33 | @Override 34 | protected boolean isLegalActionWithPreconition(GameContextPlayerView context, Set tiles) { 35 | GameStrategy strategy = context.getGameStrategy(); 36 | PlayerInfo playerInfo = context.getMyInfo(); 37 | Set remainAliveTiles = new HashSet<>(playerInfo.getAliveTiles()); 38 | remainAliveTiles.removeAll(tiles); 39 | 40 | return 41 | // 获取所有牌的流 42 | strategy.getAllTiles().stream() 43 | // 只留下id==0的牌 44 | .filter(tileToGet -> tileToGet.id() == 0) 45 | // 与打出动作牌后的aliveTiles合并,看任何一种合并后的aliveTiles能否和牌 46 | .anyMatch(tileToGet -> { 47 | WinInfo winInfo = WinInfo.fromPlayerTiles(playerInfo, tileToGet, false); 48 | winInfo.setAliveTiles(mergedSet(remainAliveTiles, tileToGet)); 49 | winInfo.setContextView(context); 50 | return strategy.canWin(winInfo); 51 | }); 52 | } 53 | 54 | @Override 55 | protected void doLegalAction(GameContext context, PlayerLocation location, Set tiles) { 56 | super.doLegalAction(context, location, tiles); 57 | context.getPlayerInfoByLocation(location).setTing(true); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/Player.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.Set; 4 | 5 | import com.github.blovemaple.mj.action.Action; 6 | import com.github.blovemaple.mj.action.PlayerAction; 7 | import com.github.blovemaple.mj.action.PlayerActionType; 8 | import com.github.blovemaple.mj.game.GameContextPlayerView; 9 | 10 | /** 11 | * 玩家。 12 | * 13 | * @author blovemaple 14 | */ 15 | public interface Player { 16 | /** 17 | * 返回玩家名字。 18 | */ 19 | public String getName(); 20 | 21 | /** 22 | * 选择一个要执行的动作。选择过程中需要检查线程中断,如果被中断则不需要继续选择(可能是限时已到,或其他玩家已做出优先级更高的动作等), 23 | * 此时抛出InterruptedException即可。
    24 | * 默认实现为调用 25 | * {@link #chooseAction(com.github.blovemaple.mj.game.GameContext.PlayerView, Set, Action)} 26 | * ,illegalAction为null。 27 | * 28 | * @param contextView 29 | * 游戏上下文 30 | * @param actionTypes 31 | * 可选动作类型 32 | * @return 要执行的动作 33 | * @throws InterruptedException 34 | * 线程被中断时抛出此异常。选择过程中随时可能被中断,实现时应该经常检查。 35 | */ 36 | public default PlayerAction chooseAction(GameContextPlayerView contextView, 37 | Set actionTypes) throws InterruptedException { 38 | return chooseAction(contextView, actionTypes, null); 39 | } 40 | 41 | /** 42 | * 选择一个要执行的动作。选择过程中需要检查线程中断,如果被中断则不需要继续选择(可能是限时已到,或其他玩家已做出优先级更高的动作等), 43 | * 此时抛出InterruptedException即可。 44 | * 45 | * @param contextView 46 | * 游戏上下文 47 | * @param actionTypes 48 | * 可选动作类型 49 | * @param illegalAction 50 | * 如果非null,则表示上一次选择的动作不符合规则,需要重新选择。此参数提供上次选择的动作。 51 | * @return 要执行的动作 52 | * @throws InterruptedException 53 | * 线程被中断时抛出此异常。选择过程中随时可能被中断,实现时应该经常检查。 54 | */ 55 | public PlayerAction chooseAction(GameContextPlayerView contextView, 56 | Set actionTypes, PlayerAction illegalAction) 57 | throws InterruptedException; 58 | 59 | /** 60 | * 完成一个动作时通知。 61 | */ 62 | void actionDone(GameContextPlayerView contextView, Action action); 63 | 64 | /** 65 | * 倒计时有变化时通知。(会通知所有玩家,被通知的玩家不一定要做动作) 66 | * 67 | * @param secondsToGo 68 | * 剩余秒数。null表示倒计时结束或取消。 69 | */ 70 | void timeLimit(GameContextPlayerView contextView, Integer secondsToGo); 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/win/CachedWinFeature.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.win; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import java.util.WeakHashMap; 9 | import java.util.function.Function; 10 | 11 | import com.github.blovemaple.mj.object.PlayerInfo; 12 | import com.github.blovemaple.mj.object.Tile; 13 | 14 | /** 15 | * 带缓存的WinFeature。默认仅使用手牌作为识别条件。识别条件相同的会优先使用缓存结果。 16 | * 17 | * @deprecated 18 | * 19 | * @author blovemaple 20 | */ 21 | public abstract class CachedWinFeature { 22 | private final Map cache = Collections.synchronizedMap(new WeakHashMap<>()); 23 | 24 | private boolean useAliveTiles = true; 25 | private final List> otherCacheKeys = new ArrayList<>(); 26 | 27 | protected void setUseAliveTiles(boolean useAliveTiles) { 28 | this.useAliveTiles = useAliveTiles; 29 | } 30 | 31 | protected void addCacheKey(Function value) { 32 | otherCacheKeys.add(value); 33 | } 34 | 35 | public boolean match(PlayerInfo playerInfo, Set aliveTiles) { 36 | Set realAliveTiles = aliveTiles != null ? aliveTiles : playerInfo.getAliveTiles(); 37 | 38 | int hash = hash(playerInfo, realAliveTiles); 39 | Boolean result = cache.get(hash); 40 | if (result == null) { 41 | result = matchWithoutCache(playerInfo, realAliveTiles); 42 | cache.put(hash, result); 43 | } 44 | return result; 45 | } 46 | 47 | private int hash(PlayerInfo playerInfo, Set realAliveTiles) { 48 | final int prime = 31; 49 | int result = 1; 50 | if (useAliveTiles) { 51 | result = prime * result + ((realAliveTiles == null) ? 0 : realAliveTiles.hashCode()); 52 | } 53 | for (Function function : otherCacheKeys) { 54 | Object value = function.apply(playerInfo); 55 | result = prime * result + ((value == null) ? 0 : value.hashCode()); 56 | } 57 | return result; 58 | } 59 | 60 | /** 61 | * 判断是否符合。未中缓存时调用。 62 | * 63 | * @param playerInfo 64 | * 除手牌之外的信息 65 | * @param realAliveTiles 66 | * 手牌 67 | * @return 是否符合 68 | */ 69 | public abstract boolean matchWithoutCache(PlayerInfo playerInfo, Set realAliveTiles); 70 | 71 | } 72 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.github.blovemaple 6 | mahjong 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | mahjong 11 | https://github.com/blovemaple/mahjong 12 | 13 | 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.jline 20 | jline-terminal 21 | 3.21.0 22 | 23 | 24 | com.google.guava 25 | guava 26 | 31.0.1-jre 27 | 28 | 29 | org.apache.commons 30 | commons-lang3 31 | 3.7 32 | 33 | 34 | junit 35 | junit 36 | 4.13.2 37 | test 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.apache.maven.plugins 45 | maven-compiler-plugin 46 | 47 | 9 48 | 9 49 | 50 | 51 | 52 | org.apache.maven.plugins 53 | maven-jar-plugin 54 | 55 | 56 | 57 | com.github.blovemaple.mj.cli.CliRunner 58 | 59 | 60 | 61 | 62 | 63 | maven-assembly-plugin 64 | 65 | 66 | 67 | com.github.blovemaple.mj.cli.CliRunner 68 | 69 | 70 | 71 | jar-with-dependencies 72 | 73 | 74 | 75 | 76 | make-assembly 77 | package 78 | 79 | single 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/PlayerAction.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | import java.util.Set; 4 | 5 | import com.github.blovemaple.mj.object.PlayerLocation; 6 | import com.github.blovemaple.mj.object.Tile; 7 | 8 | /** 9 | * 10 | * 玩家做出的动作。可以附加若干牌。 11 | * 12 | * @author blovemaple 13 | */ 14 | public class PlayerAction extends Action { 15 | 16 | private PlayerLocation location; 17 | private Set tiles; 18 | 19 | /** 20 | * 新建一个没有附加牌的实例。 21 | */ 22 | public PlayerAction(PlayerLocation location, ActionType type) { 23 | this(location, type, Set.of()); 24 | } 25 | 26 | /** 27 | * 新建有且只有一个附加牌的实例。 28 | */ 29 | public PlayerAction(PlayerLocation location, ActionType type, Tile tile) { 30 | this(location, type, Set.of(tile)); 31 | } 32 | 33 | /** 34 | * 新建有若干附加牌的实例。 35 | */ 36 | public PlayerAction(PlayerLocation location, ActionType type, Set tiles) { 37 | super(type); 38 | this.location = location; 39 | this.tiles = tiles; 40 | } 41 | 42 | /** 43 | * 返回附加牌。 44 | */ 45 | public Set getTiles() { 46 | return tiles; 47 | } 48 | 49 | /** 50 | * 返回唯一的附加牌。 51 | */ 52 | public Tile getTile() { 53 | Set tiles = getTiles(); 54 | if (tiles.isEmpty()) 55 | return null; 56 | if (tiles.size() > 1) 57 | throw new IllegalStateException("Tile count is more than 1: " + tiles.size()); 58 | return tiles.iterator().next(); 59 | } 60 | 61 | /** 62 | * 返回玩家位置。 63 | */ 64 | public PlayerLocation getLocation() { 65 | return location; 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | final int prime = 31; 71 | int result = super.hashCode(); 72 | result = prime * result + ((location == null) ? 0 : location.hashCode()); 73 | result = prime * result + ((tiles == null) ? 0 : tiles.hashCode()); 74 | return result; 75 | } 76 | 77 | @Override 78 | public boolean equals(Object obj) { 79 | if (this == obj) 80 | return true; 81 | if (!super.equals(obj)) 82 | return false; 83 | if (!(obj instanceof PlayerAction)) 84 | return false; 85 | PlayerAction other = (PlayerAction) obj; 86 | if (location != other.location) 87 | return false; 88 | if (tiles == null) { 89 | if (other.tiles != null) 90 | return false; 91 | } else if (!tiles.equals(other.tiles)) 92 | return false; 93 | return true; 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return "[" + getLocation() + ", " + getType() + ", " + tiles + "]"; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/game/GameContextPlayerViewImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.game; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | 5 | import java.util.List; 6 | 7 | import com.github.blovemaple.mj.action.Action; 8 | import com.github.blovemaple.mj.action.PlayerAction; 9 | import com.github.blovemaple.mj.object.MahjongTablePlayerView; 10 | import com.github.blovemaple.mj.object.PlayerInfo; 11 | import com.github.blovemaple.mj.object.PlayerLocation; 12 | import com.github.blovemaple.mj.object.Tile; 13 | import com.github.blovemaple.mj.rule.GameStrategy; 14 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 15 | 16 | /** 17 | * {@link GameContextPlayerView}的实现。 18 | * 19 | * @author blovemaple 20 | */ 21 | public class GameContextPlayerViewImpl implements GameContextPlayerView { 22 | 23 | private final GameContext gameContext; 24 | private final PlayerLocation myLocation; 25 | 26 | public GameContextPlayerViewImpl(GameContext gameContext, PlayerLocation myLocation) { 27 | this.gameContext = gameContext; 28 | this.myLocation = myLocation; 29 | } 30 | 31 | @Override 32 | public MahjongTablePlayerView getTableView() { 33 | return gameContext.getTable().getPlayerView(myLocation); 34 | } 35 | 36 | @Override 37 | public GameStrategy getGameStrategy() { 38 | return gameContext.getGameStrategy(); 39 | } 40 | 41 | @Override 42 | public TimeLimitStrategy getTimeLimitStrategy() { 43 | return gameContext.getTimeLimitStrategy(); 44 | } 45 | 46 | @Override 47 | public PlayerLocation getMyLocation() { 48 | return myLocation; 49 | } 50 | 51 | @Override 52 | public PlayerInfo getMyInfo() { 53 | return gameContext.getPlayerInfoByLocation(myLocation); 54 | } 55 | 56 | @Override 57 | public PlayerLocation getZhuangLocation() { 58 | return gameContext.getZhuangLocation(); 59 | } 60 | 61 | @Override 62 | public String getStageName() { 63 | return gameContext.getStage().getName(); 64 | } 65 | 66 | @Override 67 | public Action getLastAction() { 68 | return gameContext.getLastAction(); 69 | } 70 | 71 | @Override 72 | public PlayerLocation getLastActionLocation() { 73 | return gameContext.getLastActionLocation(); 74 | } 75 | 76 | @Override 77 | public Tile getJustDrawedTile() { 78 | Action la = getLastAction(); 79 | if (!(la instanceof PlayerAction)) 80 | return null; 81 | if (((PlayerAction) la).getLocation() != myLocation) 82 | return null; 83 | if (!DRAW.matchBy(la.getType())) { 84 | return null; 85 | } 86 | return getMyInfo().getLastDrawedTile(); 87 | } 88 | 89 | @Override 90 | public List getDoneActions() { 91 | return gameContext.getDoneActions(); 92 | } 93 | 94 | @Override 95 | public GameResult getGameResult() { 96 | return gameContext.getGameResult(); 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/BuhuaActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.object.TileGroupType.*; 4 | 5 | import java.util.Set; 6 | 7 | import com.github.blovemaple.mj.action.AbstractPlayerActionType; 8 | import com.github.blovemaple.mj.action.Action; 9 | import com.github.blovemaple.mj.action.ActionType; 10 | import com.github.blovemaple.mj.action.PlayerAction; 11 | import com.github.blovemaple.mj.game.GameContext; 12 | import com.github.blovemaple.mj.game.GameContextPlayerView; 13 | import com.github.blovemaple.mj.object.PlayerInfo; 14 | import com.github.blovemaple.mj.object.PlayerLocation; 15 | import com.github.blovemaple.mj.object.Tile; 16 | import com.github.blovemaple.mj.object.TileGroup; 17 | import com.github.blovemaple.mj.rule.simple.BeforePlayingStage; 18 | import com.github.blovemaple.mj.rule.simple.PlayingStage; 19 | import com.google.common.collect.Streams; 20 | 21 | /** 22 | * 动作类型“补花”。 23 | * 24 | * @author blovemaple 25 | */ 26 | public class BuhuaActionType extends AbstractPlayerActionType { 27 | 28 | protected BuhuaActionType() { 29 | } 30 | 31 | @Override 32 | public boolean canPass(GameContext context, PlayerLocation location) { 33 | return true; 34 | } 35 | 36 | protected boolean meetPrecondition(GameContextPlayerView context) { 37 | switch (context.getStageName()) { 38 | case BeforePlayingStage.NAME: 39 | // 如果在BEFORE_PLAYING阶段,须满足上一个动作非补花 40 | ActionType lastActionType = Streams 41 | .findLast(context.getDoneActions().stream() 42 | .filter(action -> (action instanceof PlayerAction) 43 | && ((PlayerAction) action).getLocation() == context.getMyLocation())) 44 | .map(Action::getType).orElse(null); 45 | if (lastActionType != null && this.matchBy(lastActionType)) 46 | return false; 47 | break; 48 | case PlayingStage.NAME: 49 | // 如果在PLAYING阶段,须满足aliveTiles为待出牌的状态 50 | int aliveTileSize = context.getMyInfo().getAliveTiles().size(); 51 | if (aliveTileSize % 3 != 2) 52 | return false; 53 | break; 54 | default: 55 | return false; 56 | } 57 | return true; 58 | } 59 | 60 | @Override 61 | protected int getActionTilesSize() { 62 | return BUHUA_GROUP.size(); 63 | } 64 | 65 | @Override 66 | protected boolean isLegalActionWithPreconition(GameContextPlayerView context, 67 | Set tiles) { 68 | return BUHUA_GROUP.isLegalTiles(tiles); 69 | } 70 | 71 | @Override 72 | protected void doLegalAction(GameContext context, PlayerLocation location, 73 | Set tiles) { 74 | PlayerInfo playerInfo = context.getPlayerInfoByLocation(location); 75 | playerInfo.getAliveTiles().removeAll(tiles); 76 | playerInfo.getTileGroups().add(new TileGroup(BUHUA_GROUP, tiles)); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/win/WinType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.win; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | import java.util.Set; 6 | import java.util.stream.Stream; 7 | 8 | import com.github.blovemaple.mj.object.PlayerInfo; 9 | import com.github.blovemaple.mj.object.Tile; 10 | import com.github.blovemaple.mj.object.TileType; 11 | import com.github.blovemaple.mj.object.TileUnit; 12 | 13 | /** 14 | * 和牌类型。 15 | * 16 | * @author blovemaple 17 | */ 18 | public interface WinType { 19 | 20 | /** 21 | * 全部解析成可以和牌的完整的TileUnit集合的列表并填入和牌信息并返回,失败填入并返回空列表。 22 | * 23 | * @param winInfo 24 | * 和牌信息 25 | */ 26 | public List> parseWinTileUnits(WinInfo winInfo); 27 | 28 | /** 29 | * 判断根据此和牌类型是否可以和牌。如果可以和牌,则向winInfo填入和牌信息。
    30 | * 默认实现为调用parseWinTileUnits解析。 31 | * 32 | * @param winInfo 33 | * 和牌信息 34 | * @return 是否可以和牌 35 | */ 36 | public default boolean match(WinInfo winInfo) { 37 | List> units = parseWinTileUnits(winInfo); 38 | return units != null && !units.isEmpty(); 39 | } 40 | 41 | /** 42 | * 用于机器人,返回建议打出的牌,即从手牌中排除掉明显不应该打出的牌并返回。返回的列表按建议的优先级从高到低排列。 43 | */ 44 | public List getDiscardCandidates(Set aliveTiles, Collection candidates); 45 | 46 | /** 47 | * 用于机器人,获取ChangingForWin的流,移除changeCount个牌,增加(changeCount+1)个牌。 48 | */ 49 | public Stream changingsForWin(PlayerInfo playerInfo, int changeCount, Collection candidates); 50 | 51 | /** 52 | * 一种结果是和牌的换牌方法,移除removedTiles并增加addedTiles。 53 | * 54 | * @author blovemaple 55 | */ 56 | public static class ChangingForWin { 57 | public Set removedTiles, addedTiles; 58 | private int hashCode; 59 | 60 | public ChangingForWin(Set removedTiles, Set addedTiles) { 61 | this.removedTiles = removedTiles; 62 | this.addedTiles = addedTiles; 63 | } 64 | 65 | @Override 66 | public int hashCode() { 67 | if (hashCode == 0) { 68 | final int prime = 31; 69 | int result = 1; 70 | result = prime * result + addedTiles.stream().map(Tile::type).mapToInt(TileType::hashCode).sum(); 71 | result = prime * result + removedTiles.stream().map(Tile::type).mapToInt(TileType::hashCode).sum(); 72 | hashCode = result; 73 | } 74 | return hashCode; 75 | } 76 | 77 | @Override 78 | public boolean equals(Object obj) { 79 | if (this == obj) 80 | return true; 81 | if (obj == null) 82 | return false; 83 | if (!(obj instanceof ChangingForWin)) 84 | return false; 85 | ChangingForWin other = (ChangingForWin) obj; 86 | return hashCode() == other.hashCode(); 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "ChangingForWin [removedTiles=" + removedTiles + ", addedTiles=" + addedTiles + "]"; 92 | } 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.io.Serializable; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | import com.github.blovemaple.mj.object.TileRank.NumberRank; 11 | 12 | /** 13 | * 牌型。每个牌型的牌通常有四张。 14 | * 15 | * @author blovemaple 16 | */ 17 | public class TileType implements Serializable, Comparable { 18 | private static final long serialVersionUID = 1L; 19 | 20 | /** 21 | * 字牌的占位大小值。 22 | */ 23 | public static final int HONOR_RANK = 0; 24 | 25 | private static final List all; 26 | private static final Map, TileType>> map; 27 | static { 28 | // 初始化所有牌型 29 | all = Collections.unmodifiableList( // 30 | Stream.of(TileSuit.values()) 31 | .flatMap(suit -> suit.getAllRanks().stream().map(rank -> new TileType(suit, rank))) 32 | .collect(Collectors.toList())); 33 | map = all.stream().collect( // 34 | Collectors.groupingBy(TileType::suit, // 35 | Collectors.groupingBy(TileType::rank, // 36 | Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))))); 37 | } 38 | 39 | /** 40 | * 返回所有牌型的列表。 41 | */ 42 | public static List all() { 43 | return all; 44 | } 45 | 46 | /** 47 | * 返回指定牌型。 48 | */ 49 | public static TileType of(TileSuit suit, TileRank rank) { 50 | return map.get(suit).get(rank); 51 | } 52 | 53 | private final TileSuit suit; 54 | private final TileRank rank; 55 | 56 | private TileType(TileSuit suit, TileRank rank) { 57 | this.suit = suit; 58 | this.rank = rank; 59 | } 60 | 61 | /** 62 | * 返回花色。 63 | */ 64 | public TileSuit suit() { 65 | return suit; 66 | } 67 | 68 | /** 69 | * 返回种类。 70 | */ 71 | public TileRank rank() { 72 | return rank; 73 | } 74 | 75 | /** 76 | * 返回种类是否是数字。 77 | */ 78 | public boolean isNumberRank() { 79 | return rank.getClass()==NumberRank.class; 80 | } 81 | 82 | /** 83 | * 返回数字种类的数值。 84 | * 85 | * @throws UnsupportedOperationException 86 | * 种类不是数字 87 | */ 88 | public int number() { 89 | if (!isNumberRank()) 90 | throw new UnsupportedOperationException("No number for a non-number rank."); 91 | return ((NumberRank) rank).number(); 92 | } 93 | 94 | @Override 95 | public int compareTo(TileType o) { 96 | if (this == o) 97 | return 0; 98 | int suitRes = this.suit().compareTo(o.suit()); 99 | if (suitRes != 0) 100 | return suitRes; 101 | return TileRank.compare(this.rank(), o.rank()); 102 | } 103 | 104 | /** 105 | * Just for debug. 106 | * 107 | * @see java.lang.Object#toString() 108 | */ 109 | @Override 110 | public String toString() { 111 | if (suit.getRankClass() == NumberRank.class) 112 | return rank.toString() + " " + suit; 113 | else 114 | return rank.toString(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/BugangActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.object.TileGroupType.*; 4 | import static com.github.blovemaple.mj.utils.MyUtils.*; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.Set; 9 | import java.util.function.Predicate; 10 | 11 | import com.github.blovemaple.mj.action.AbstractPlayerActionType; 12 | import com.github.blovemaple.mj.game.GameContext; 13 | import com.github.blovemaple.mj.game.GameContextPlayerView; 14 | import com.github.blovemaple.mj.object.PlayerInfo; 15 | import com.github.blovemaple.mj.object.PlayerLocation; 16 | import com.github.blovemaple.mj.object.Tile; 17 | import com.github.blovemaple.mj.object.TileGroup; 18 | 19 | /** 20 | * 动作类型“补杠”。 21 | * 22 | * @author blovemaple 23 | */ 24 | public class BugangActionType extends AbstractPlayerActionType { 25 | 26 | protected BugangActionType() { 27 | } 28 | 29 | @Override 30 | public boolean canPass(GameContext context, PlayerLocation location) { 31 | return true; 32 | } 33 | 34 | @Override 35 | protected Predicate getAliveTileSizePrecondition() { 36 | return size -> size % 3 == 2; 37 | } 38 | 39 | @Override 40 | protected int getActionTilesSize() { 41 | return 1; 42 | } 43 | 44 | @Override 45 | protected boolean isLegalActionWithPreconition(GameContextPlayerView context, 46 | Set tiles) { 47 | return findLegalPengGroup(context.getMyInfo(), tiles).isPresent(); 48 | } 49 | 50 | @Override 51 | protected void doLegalAction(GameContext context, PlayerLocation location, 52 | Set tiles) { 53 | PlayerInfo playerInfo = context.getPlayerInfoByLocation(location); 54 | 55 | TileGroup group = findLegalPengGroup(playerInfo, tiles).orElse(null); 56 | if (group == null) 57 | // tiles不合法,抛异常,因为调用此方法时应该确保是合法的 58 | throw new IllegalArgumentException( 59 | "Illegal bugang tiles: " + tiles); 60 | 61 | // 在aliveTiles中去掉动作牌 62 | playerInfo.getAliveTiles().removeAll(tiles); 63 | 64 | // 把碰组改为补杠组,并加上动作牌 65 | TileGroup newGroup = new TileGroup(BUGANG_GROUP, group.getGotTile(), 66 | group.getFromRelation(), mergedSet(group.getTiles(), tiles)); 67 | List groups = playerInfo.getTileGroups(); 68 | int groupIndex = groups.indexOf(group); 69 | groups.remove(groupIndex); 70 | groups.add(groupIndex, newGroup); 71 | } 72 | 73 | /** 74 | * 返回在玩家的牌中能与动作牌组成补杠的碰组(Optional)。 75 | */ 76 | private Optional findLegalPengGroup(PlayerInfo playerInfo, 77 | Set actionTiles) { 78 | return playerInfo.getTileGroups() 79 | // 过滤出该玩家的所有碰组 80 | .stream().filter(group -> group.getType() == PENG_GROUP) 81 | // 过滤出能与动作相关牌组成合法补杠的 82 | .filter(group -> { 83 | // 取出碰组的牌,并加上动作中的tiles(应该只有一个tile) 84 | Set gangTiles = mergedSet(group.getTiles(), 85 | actionTiles); 86 | // 只留下合法的(补)杠 87 | return BUGANG_GROUP.isLegalTiles(gangTiles); 88 | }) 89 | // 取任何一个(有的话肯定只有一个) 90 | .findAny(); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/GameStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule; 2 | 3 | import java.util.Comparator; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import com.github.blovemaple.mj.action.Action; 9 | import com.github.blovemaple.mj.action.ActionTypeAndLocation; 10 | import com.github.blovemaple.mj.action.PlayerAction; 11 | import com.github.blovemaple.mj.action.PlayerActionType; 12 | import com.github.blovemaple.mj.game.GameContext; 13 | import com.github.blovemaple.mj.object.MahjongTable; 14 | import com.github.blovemaple.mj.object.PlayerLocation; 15 | import com.github.blovemaple.mj.object.Tile; 16 | import com.github.blovemaple.mj.rule.win.FanType; 17 | import com.github.blovemaple.mj.rule.win.WinInfo; 18 | import com.github.blovemaple.mj.rule.win.WinType; 19 | 20 | /** 21 | * 游戏策略。即一种游戏规则的定义。 22 | * 23 | * @author blovemaple 24 | */ 25 | public interface GameStrategy { 26 | 27 | /** 28 | * 检查一个麻将桌是否符合条件开始进行一局。 29 | * 30 | * @param table 31 | * 麻将桌 32 | * @return 如果可以开始,返回true,否则返回false。 33 | */ 34 | public boolean checkReady(MahjongTable table); 35 | 36 | /** 37 | * 获取全部麻将牌的列表。 38 | */ 39 | public List getAllTiles(); 40 | 41 | /** 42 | * 根据阶段名称返回阶段。 43 | */ 44 | public GameStage getStageByName(String stageName); 45 | 46 | /** 47 | * 返回初始阶段。 48 | */ 49 | public GameStage getFirstStage(); 50 | 51 | /** 52 | * 在一局开始之前对上下文进行必要操作。 53 | */ 54 | public void readyContext(GameContext context); 55 | 56 | /** 57 | * 获取动作优先级比较器。优先级越高的越小。 58 | */ 59 | public Comparator getActionPriorityComparator(); 60 | 61 | /** 62 | * 根据当前状态返回指定玩家超时默认做的动作。 63 | * 64 | * @return 默认动作,null表示不做动作 65 | */ 66 | public PlayerAction getPlayerDefaultAction(GameContext context, PlayerLocation location, 67 | Set choises); 68 | 69 | /** 70 | * 根据当前状态返回默认动作。默认动作是所有玩家都没有可选动作或均选择不做动作之后自动执行的动作。 71 | * 72 | * @return 默认动作 73 | */ 74 | public Action getDefaultAction(GameContext context, Map> choises); 75 | 76 | /** 77 | * 获取此策略支持的所有和牌类型。 78 | */ 79 | public List getAllWinTypes(); 80 | 81 | /** 82 | * 判断指定条件下是否可和牌。
    83 | * 默认实现为使用此策略支持的所有和牌类型进行判断,至少有一种和牌类型判断可以和牌则可以和牌。 84 | */ 85 | public default boolean canWin(WinInfo winInfo) { 86 | return getAllWinTypes().stream().anyMatch(winType -> winType.match(winInfo)); 87 | // TODO 缓存 88 | } 89 | 90 | /** 91 | * 获取此策略支持的所有番种。返回的列表中,被覆盖的番种必须在覆盖它的番种之后。不允许两个番种相互覆盖。 92 | */ 93 | public List getAllFanTypes(); 94 | 95 | /** 96 | * 检查和牌的所有番种和番数。
    97 | * 默认实现为使用此策略支持的所有番种和番数进行统计。 98 | */ 99 | public default Map getFans(WinInfo winInfo) { 100 | return FanType.getFans(winInfo, getAllFanTypes(), getAllWinTypes()); 101 | } 102 | 103 | /** 104 | * 根据当前状态判断游戏是否结束。 105 | */ 106 | public boolean tryEndGame(GameContext context); 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/bazbot/BazBotAliveTiles.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.bazbot; 2 | 3 | import static com.github.blovemaple.mj.local.bazbot.BazBotTileUnit.BazBotTileUnitType.*; 4 | import static java.util.Comparator.*; 5 | import static java.util.stream.Collectors.*; 6 | 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.concurrent.ExecutionException; 10 | import java.util.logging.Logger; 11 | import java.util.stream.Stream; 12 | 13 | import com.github.blovemaple.mj.object.Tile; 14 | import com.github.blovemaple.mj.object.TileType; 15 | import com.google.common.cache.Cache; 16 | import com.google.common.cache.CacheBuilder; 17 | 18 | /** 19 | * BazBot手中的活牌。用于计算{@link #tileTypesToWin()}。有缓存。 20 | * 21 | * @author blovemaple 22 | */ 23 | class BazBotAliveTiles { 24 | private static final Logger logger = Logger.getLogger(BazBotAliveTiles.class.getSimpleName()); 25 | 26 | private static final Cache, BazBotAliveTiles> cache = // 27 | CacheBuilder.newBuilder().maximumSize(20).build(); 28 | 29 | public static BazBotAliveTiles of(Set aliveTiles) { 30 | try { 31 | if (cache.getIfPresent(aliveTiles) != null) 32 | logger.fine(() -> "BazBotAliveTiles Cache hit."); 33 | else 34 | logger.fine(() -> "BazBotAliveTiles Cache NO hit."); 35 | return cache.get(aliveTiles, () -> new BazBotAliveTiles(aliveTiles)); 36 | } catch (ExecutionException e) { 37 | // not possible 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | 42 | private final Set aliveTiles; 43 | private List neighborhoods; // lazy 44 | private List> tileTypesToWin; // lazy 45 | 46 | private BazBotAliveTiles(Set aliveTiles) { 47 | this.aliveTiles = aliveTiles; 48 | } 49 | 50 | public List neighborhoods() { 51 | tileTypesToWin(); 52 | return neighborhoods; 53 | } 54 | 55 | public List> tileTypesToWin() { 56 | if (tileTypesToWin != null) 57 | return tileTypesToWin; 58 | 59 | synchronized (this) { 60 | if (tileTypesToWin != null) 61 | return tileTypesToWin; 62 | 63 | neighborhoods = BazBotTileNeighborhood.parse(aliveTiles); 64 | int forShunkeCount = aliveTiles.size() / 3; 65 | 66 | tileTypesToWin = Stream.of(new BazBotChoosingTileUnits(neighborhoods, forShunkeCount)) // 一个初始units,为flatmap做准备 67 | .flatMap(units -> units.newToChoose(COMPLETE_JIANG, true)) // 选所有完整将牌,以及不选完整将牌 68 | .flatMap(units -> units.newToChoose(COMPLETE_SHUNKE, false)) // 选所有合适的完整顺刻组合 69 | .flatMap(units -> units.newToChoose(UNCOMPLETE_SHUNKE_FOR_ONE, false)) // 选所有合适的不完整顺刻组合(缺一张的) 70 | .flatMap(units -> units.newToChoose(UNCOMPLETE_SHUNKE_FOR_TWO, false)) // 选所有合适的不完整顺刻组合(缺两张的) 71 | .flatMap(units -> units.newToChoose(UNCOMPLETE_JIANG, false)) // 选所有合适的不完整将牌 72 | // .peek(System.out::println) 73 | .flatMap(BazBotChoosingTileUnits::tileTypesToWin) // 计算tileUnits和牌所需牌型 74 | // .peek(System.out::println) 75 | .peek(tileTypes -> tileTypes.sort(naturalOrder())) // 每组tileType内部排序,准备去重 76 | .distinct() // 去重 77 | .collect(toList()) // 收集结果 78 | ; 79 | 80 | return tileTypesToWin; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/PlayerActionTypes.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.object.PlayerLocation.Relation.*; 4 | import static com.github.blovemaple.mj.object.TileGroupType.*; 5 | 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.Set; 9 | 10 | import com.github.blovemaple.mj.action.Action; 11 | import com.github.blovemaple.mj.action.ActionType; 12 | import com.github.blovemaple.mj.action.IllegalActionException; 13 | import com.github.blovemaple.mj.action.PlayerActionType; 14 | import com.github.blovemaple.mj.game.GameContext; 15 | import com.github.blovemaple.mj.game.GameContextPlayerView; 16 | import com.github.blovemaple.mj.object.PlayerLocation; 17 | import com.github.blovemaple.mj.object.Tile; 18 | 19 | /** 20 | * 玩家动作类型枚举。
    21 | * 枚举的每种动作类型包含对应Type类的单例,并委托调用其对应的方法。 22 | * 23 | * @author blovemaple 24 | */ 25 | public enum PlayerActionTypes implements PlayerActionType { 26 | /** 27 | * 吃 28 | */ 29 | CHI(new CpgActionType(CHI_GROUP, Collections.singleton(PREVIOUS))), 30 | /** 31 | * 碰 32 | */ 33 | PENG(new CpgActionType(PENG_GROUP)), 34 | /** 35 | * 直杠 36 | */ 37 | ZHIGANG(new CpgActionType(ZHIGANG_GROUP)), 38 | /** 39 | * 补杠 40 | */ 41 | BUGANG(new BugangActionType()), 42 | /** 43 | * 暗杠 44 | */ 45 | ANGANG(new AngangActionType()), 46 | /** 47 | * 补花 48 | */ 49 | BUHUA(new BuhuaActionType()), 50 | /** 51 | * 打牌 52 | */ 53 | DISCARD(new DiscardActionType()), 54 | /** 55 | * 打牌的同时听牌 56 | */ 57 | DISCARD_WITH_TING(new DiscardWithTingActionType()), 58 | /** 59 | * 摸牌 60 | */ 61 | DRAW(new DrawActionType()), 62 | /** 63 | * 摸底牌 64 | */ 65 | DRAW_BOTTOM(new DrawBottomActionType()), 66 | /** 67 | * 和牌 68 | */ 69 | WIN(new WinActionType()); 70 | 71 | private final PlayerActionType type; 72 | 73 | private PlayerActionTypes(PlayerActionType type) { 74 | this.type = type; 75 | } 76 | 77 | // 以下都是委托方法,调用type的对应方法 78 | 79 | @Override 80 | public boolean canDo(GameContext context, PlayerLocation location) { 81 | return type.canDo(context, location); 82 | } 83 | 84 | @Override 85 | public boolean canPass(GameContext context, PlayerLocation location) { 86 | return type.canPass(context, location); 87 | } 88 | 89 | @Override 90 | public Collection> getLegalActionTiles(GameContextPlayerView context) { 91 | return type.getLegalActionTiles(context); 92 | } 93 | 94 | @Override 95 | public void doAction(GameContext context, Action action) throws IllegalActionException { 96 | type.doAction(context, action); 97 | } 98 | 99 | @Override 100 | public boolean matchBy(ActionType testType) { 101 | return type.matchBy(testType); 102 | } 103 | 104 | @Override 105 | public Class getRealTypeClass() { 106 | return type.getRealTypeClass(); 107 | } 108 | 109 | @Override 110 | public ActionType getRealTypeObject() { 111 | return type; 112 | } 113 | 114 | @Override 115 | public boolean isLegalAction(GameContext context, Action action) { 116 | return type.isLegalAction(context, action); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/PlayerInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * 麻将桌上一个玩家的信息,包括玩家对象、牌,以及其他信息。 11 | * 12 | * @author blovemaple 13 | */ 14 | public class PlayerInfo extends PlayerTiles implements Cloneable { 15 | /** 16 | * 玩家。 17 | */ 18 | private Player player = null; 19 | /** 20 | * 最后摸的牌。 21 | */ 22 | private Tile lastDrawedTile = null; 23 | /** 24 | * 已经打出的牌。 25 | */ 26 | private List discardedTiles = new ArrayList<>(); 27 | /** 28 | * 是否听和。 29 | */ 30 | private boolean isTing = false; 31 | 32 | public Player getPlayer() { 33 | return player; 34 | } 35 | 36 | public void setPlayer(Player player) { 37 | this.player = player; 38 | } 39 | 40 | public Tile getLastDrawedTile() { 41 | return lastDrawedTile; 42 | } 43 | 44 | public void setLastDrawedTile(Tile lastDrawedTile) { 45 | this.lastDrawedTile = lastDrawedTile; 46 | } 47 | 48 | public List getDiscardedTiles() { 49 | return discardedTiles; 50 | } 51 | 52 | public void setDiscardedTiles(List discardedTiles) { 53 | this.discardedTiles = discardedTiles; 54 | } 55 | 56 | public boolean isTing() { 57 | return isTing; 58 | } 59 | 60 | public void setTing(boolean isTing) { 61 | this.isTing = isTing; 62 | } 63 | 64 | /** 65 | * 清空玩家的牌,回到初始状态。 66 | */ 67 | public void clear() { 68 | aliveTiles.clear(); 69 | lastDrawedTile = null; 70 | discardedTiles.clear(); 71 | tileGroups.clear(); 72 | isTing = false; 73 | } 74 | 75 | public PlayerInfo clone() { 76 | PlayerInfo c; 77 | try { 78 | c = (PlayerInfo) super.clone(); 79 | } catch (CloneNotSupportedException e) { 80 | // 不可能,因为PlayerInfo已经实现了Cloneable 81 | throw new RuntimeException(e); 82 | } 83 | // deep copy 84 | c.aliveTiles = new HashSet<>(aliveTiles); 85 | c.discardedTiles = new ArrayList<>(discardedTiles); 86 | c.tileGroups = new ArrayList<>(tileGroups); 87 | return c; 88 | } 89 | 90 | private PlayerView otherPlayerView; 91 | 92 | /** 93 | * 获取其他玩家的视图。 94 | */ 95 | public PlayerInfoPlayerView getOtherPlayerView() { 96 | if (otherPlayerView == null) { // 不需要加锁,因为多创建了也没事 97 | otherPlayerView = new PlayerView(); 98 | } 99 | return otherPlayerView; 100 | } 101 | 102 | private class PlayerView implements PlayerInfoPlayerView { 103 | 104 | @Override 105 | public String getPlayerName() { 106 | Player player = getPlayer(); 107 | return player != null ? getPlayer().getName() : null; 108 | } 109 | 110 | @Override 111 | public int getAliveTileSize() { 112 | return getAliveTiles().size(); 113 | } 114 | 115 | @Override 116 | public List getDiscardedTiles() { 117 | return Collections.unmodifiableList(discardedTiles); 118 | } 119 | 120 | @Override 121 | public List getTileGroups() { 122 | return tileGroups.stream().map(TileGroup::getOtherPlayerView).collect(Collectors.toList()); 123 | } 124 | 125 | @Override 126 | public boolean isTing() { 127 | return isTing; 128 | } 129 | 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/utils/LambdaUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.utils; 2 | 3 | import java.util.function.BiConsumer; 4 | import java.util.function.BiFunction; 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | import java.util.function.Predicate; 8 | import java.util.function.Supplier; 9 | 10 | public class LambdaUtils { 11 | 12 | @FunctionalInterface 13 | public interface Consumer_WithExceptions { 14 | void accept(T t) throws E; 15 | } 16 | 17 | @FunctionalInterface 18 | public interface BiConsumer_WithExceptions { 19 | void accept(T t, U u) throws E; 20 | } 21 | 22 | @FunctionalInterface 23 | public interface Function_WithExceptions { 24 | R apply(T t) throws E; 25 | } 26 | 27 | @FunctionalInterface 28 | public interface BiFunction_WithExceptions { 29 | R apply(T t, U u) throws E; 30 | } 31 | 32 | @FunctionalInterface 33 | public interface Predicate_WithExceptions { 34 | boolean apply(T t) throws E; 35 | } 36 | 37 | @FunctionalInterface 38 | public interface Supplier_WithExceptions { 39 | T get() throws E; 40 | } 41 | 42 | public static Consumer rethrowConsumer( 43 | Consumer_WithExceptions consumer) throws E { 44 | return t -> { 45 | try { 46 | consumer.accept(t); 47 | } catch (Exception exception) { 48 | throwActualException(exception); 49 | } 50 | }; 51 | } 52 | 53 | public static BiConsumer rethrowBiConsumer( 54 | BiConsumer_WithExceptions biConsumer) throws E { 55 | return (t, u) -> { 56 | try { 57 | biConsumer.accept(t, u); 58 | } catch (Exception exception) { 59 | throwActualException(exception); 60 | } 61 | }; 62 | } 63 | 64 | public static Function rethrowFunction( 65 | Function_WithExceptions function) throws E { 66 | return t -> { 67 | try { 68 | return function.apply(t); 69 | } catch (Exception exception) { 70 | throwActualException(exception); 71 | return null; 72 | } 73 | }; 74 | } 75 | 76 | public static BiFunction rethrowBiFunction( 77 | BiFunction_WithExceptions function) throws E { 78 | return (t, u) -> { 79 | try { 80 | return function.apply(t, u); 81 | } catch (Exception exception) { 82 | throwActualException(exception); 83 | return null; 84 | } 85 | }; 86 | } 87 | 88 | public static Predicate rethrowPredicate( 89 | Predicate_WithExceptions predicate) throws E { 90 | return t -> { 91 | try { 92 | return predicate.apply(t); 93 | } catch (Exception exception) { 94 | throwActualException(exception); 95 | return false; 96 | } 97 | }; 98 | } 99 | 100 | public static Supplier rethrowSupplier( 101 | Supplier_WithExceptions supplier) throws E { 102 | return () -> { 103 | try { 104 | return supplier.get(); 105 | } catch (Exception exception) { 106 | throwActualException(exception); 107 | return null; 108 | } 109 | }; 110 | } 111 | 112 | @SuppressWarnings("unchecked") 113 | private static void throwActualException( 114 | Exception exception) throws E { 115 | throw (E) exception; 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/cli/CliRunner.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.cli; 2 | 3 | import java.io.IOException; 4 | import java.net.URISyntaxException; 5 | import java.util.logging.Level; 6 | import java.util.logging.LogManager; 7 | import java.util.logging.Logger; 8 | 9 | import com.github.blovemaple.mj.cli.CliView.CharHandler; 10 | import com.github.blovemaple.mj.local.LocalGame; 11 | 12 | import static com.github.blovemaple.mj.cli.CliView.CharHandler.HandlingResult.*; 13 | import static com.github.blovemaple.mj.utils.LanguageManager.ExtraMessage.*; 14 | import static com.github.blovemaple.mj.utils.LambdaUtils.*; 15 | 16 | /** 17 | * 命令行入口。 18 | * 19 | * @author blovemaple 20 | */ 21 | public class CliRunner { 22 | private static final Logger logger = Logger.getLogger(CliRunner.class.getSimpleName()); 23 | 24 | public static void main(String[] args) throws IOException, URISyntaxException { 25 | // 让日志输出到文件,不要在控制台显示 26 | LogManager.getLogManager().readConfiguration(CliRunner.class.getResource("/logging.properties").openStream()); 27 | 28 | logger.info("Started"); 29 | 30 | try { 31 | new CliRunner().run(); 32 | } catch (InterruptedException e) { 33 | } 34 | 35 | System.out.println(); 36 | System.exit(0); 37 | } 38 | 39 | private final CliView cliView; 40 | private final String myName; 41 | 42 | private LocalGame localGame; 43 | 44 | private static final char NEW_GAME_YES = 'y', NEW_GAME_NO = 'n'; 45 | 46 | public CliRunner() throws IOException, InterruptedException { 47 | cliView = new CliView(System.out, System.in); 48 | myName = "Player"; // TODO System.getProperty("user.name", "Player"); 49 | } 50 | 51 | public void run() throws InterruptedException { 52 | try { 53 | cliView.init(); 54 | printHead(); 55 | logger.info("start setup."); 56 | setup(); 57 | logger.info("end setup."); 58 | play(); 59 | } catch (Exception e) { 60 | try { 61 | logger.log(Level.SEVERE, e.toString(), e); 62 | cliView.printMessage("[ERROR] " + e.toString()); 63 | } catch (IOException e1) { 64 | logger.log(Level.SEVERE, e.toString(), e); 65 | } 66 | } 67 | } 68 | 69 | private void printHead() throws IOException { 70 | cliView.printSplitLine("MAHJONG", 50); 71 | 72 | StringBuilder head = new StringBuilder(); 73 | head.append("Welcome, ").append(myName).append("!"); 74 | head.append("\n"); 75 | head.append(GITHUB_TIP.str()); 76 | head.append("\n"); 77 | head.append(MOVE_TIP.str()); 78 | cliView.printMessage(head.toString()); 79 | 80 | cliView.printSplitLine(null, 50); 81 | } 82 | 83 | private void setup() throws InterruptedException { 84 | localGame = new LocalGame(new CliPlayer(myName, cliView), rethrowSupplier(() -> { 85 | cliView.updateStatus(NEW_GAME_QUESTION.str()); 86 | cliView.addCharHandler(NEW_GAME_CHAR_HANDLER, true); 87 | return newGame; 88 | })); 89 | } 90 | 91 | private void play() throws IOException, InterruptedException { 92 | logger.info("start play."); 93 | localGame.play(); 94 | logger.info("end play."); 95 | } 96 | 97 | @SuppressWarnings("unused") 98 | private class SettingCharHandler implements CharHandler { 99 | 100 | @Override 101 | public HandlingResult handle(char c) { 102 | return QUIT; 103 | } 104 | 105 | } 106 | 107 | private boolean newGame; 108 | 109 | private final CharHandler NEW_GAME_CHAR_HANDLER = c -> { 110 | switch (c) { 111 | case NEW_GAME_YES: 112 | newGame = true; 113 | return QUIT; 114 | case NEW_GAME_NO: 115 | newGame = false; 116 | return QUIT; 117 | default: 118 | return IGNORE; 119 | } 120 | }; 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/barbot/BarBotSimChanging.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.barbot; 2 | 3 | import static com.github.blovemaple.mj.object.StandardTileUnitType.*; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.Comparator; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | import com.github.blovemaple.mj.object.Tile; 12 | import com.github.blovemaple.mj.object.TileUnit; 13 | import com.github.blovemaple.mj.rule.win.WinInfo; 14 | 15 | /** 16 | * @author blovemaple 17 | */ 18 | @Deprecated 19 | class BarBotSimChanging { 20 | private BarBotCpgdChoice choice; 21 | 22 | private Collection removedTiles; 23 | private Collection addedTiles; 24 | 25 | private Set aliveTiles; 26 | 27 | private WinInfo winInfo; 28 | private Integer winPoint; 29 | private Double prob; 30 | 31 | public BarBotSimChanging(BarBotCpgdChoice choice, Collection removedTiles, Collection addedTiles) { 32 | this.choice = choice; 33 | this.removedTiles = removedTiles; 34 | this.addedTiles = addedTiles; 35 | } 36 | 37 | protected Collection getRemovedTiles() { 38 | return removedTiles; 39 | } 40 | 41 | protected Collection getAddedTiles() { 42 | return addedTiles; 43 | } 44 | 45 | private WinInfo getWinInfo() { 46 | if (winInfo == null) { 47 | WinInfo winInfo = WinInfo.fromPlayerTiles(choice.getPlayerInfo(), null, false); 48 | winInfo.setAliveTiles(getAliveTiles()); 49 | this.winInfo = winInfo; 50 | } 51 | return winInfo; 52 | } 53 | 54 | public boolean isWin() { 55 | return getWinPoint() > 0; 56 | } 57 | 58 | public Integer getWinPoint() { 59 | if (winPoint == null) { 60 | winPoint = choice.getBaseContextView().getGameStrategy().getFans(getWinInfo()).values().stream() 61 | .mapToInt(f -> f).sum(); 62 | } 63 | return winPoint; 64 | } 65 | 66 | public void setWinPoint(Integer winPoint) { 67 | this.winPoint = winPoint; 68 | } 69 | 70 | public double getProb() { 71 | if (prob == null) { 72 | double addedTilesProb = choice.getTask().getProb(addedTiles); 73 | prob = choice.getForWinTypes().stream() 74 | // 取choice对应的所有和牌类型解析成的所有unit集合 75 | .flatMap(winType -> winType.parseWinTileUnits(getWinInfo()) 76 | .stream()) 77 | // 取最大的系数 78 | .map(this::getRatio).max(Comparator.naturalOrder()) 79 | // 乘以概率 80 | .map(ratio -> ratio * addedTilesProb).orElse(0d); 81 | } 82 | return prob; 83 | } 84 | 85 | // 系数= 1 * 4^涉及的刻子数 * 2^涉及的顺子数 86 | private int getRatio(Collection units) { 87 | return units.stream().filter(unit -> !Collections.disjoint(unit.getTiles(), addedTiles)).map(TileUnit::getType) 88 | .reduce(1, (r, t) -> r * (t == KEZI ? 4 : t == SHUNZI ? 2 : 1), Math::multiplyExact); 89 | } 90 | 91 | private Set getAliveTiles() { 92 | if (aliveTiles == null) { 93 | aliveTiles = new HashSet<>(choice.getPlayerInfo().getAliveTiles()); 94 | aliveTiles.removeAll(removedTiles); 95 | aliveTiles.addAll(addedTiles); 96 | } 97 | return aliveTiles; 98 | } 99 | 100 | public boolean isCovered(Collection removedTiles, Collection addedTiles) { 101 | if (removedTiles.size() < this.removedTiles.size()) 102 | return false; 103 | if (addedTiles.size() < this.addedTiles.size()) 104 | return false; 105 | return removedTiles.containsAll(this.removedTiles) && addedTiles.containsAll(this.addedTiles); 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | return "[prob=" + prob + ", removedTiles=" + removedTiles + ", addedTiles=" + addedTiles + "]"; 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/bazbot/BazBotChoosingTileUnits.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.bazbot; 2 | 3 | import static com.github.blovemaple.mj.utils.MyUtils.*; 4 | import java.util.ArrayList; 5 | import java.util.Collection; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.stream.Stream; 10 | 11 | import org.apache.commons.lang3.mutable.MutableObject; 12 | 13 | import com.github.blovemaple.mj.local.bazbot.BazBotTileUnit.BazBotTileUnitType; 14 | import com.github.blovemaple.mj.object.Tile; 15 | import com.github.blovemaple.mj.object.TileType; 16 | 17 | /** 18 | * BazBot从自己的牌中挑选出的unit组合。最终用于计算tileTypesToWin。 19 | * 20 | * @author blovemaple 21 | */ 22 | class BazBotChoosingTileUnits extends BazBotTileUnits { 23 | /** 24 | * 是否缺少将牌。 25 | */ 26 | private boolean forJiang; 27 | /** 28 | * 缺少的顺刻数。 29 | */ 30 | private int forShunkeCount; 31 | 32 | /** 33 | * 构建一个初始的TileUnits,没有选择任何unit。 34 | */ 35 | public BazBotChoosingTileUnits(Collection neighborhoods, int forShunkeCount) { 36 | super(neighborhoods); 37 | forJiang = true; 38 | this.forShunkeCount = forShunkeCount; 39 | } 40 | 41 | /** 42 | * 从指定的TileUnits和要添加的units构建实例。 43 | * 44 | * @param original 45 | * 原始TileUnits 46 | * @param newUnits 47 | * 要添加的units 48 | */ 49 | private BazBotChoosingTileUnits(BazBotChoosingTileUnits original, BazBotTileUnits newUnits) { 50 | super(original, newUnits); 51 | 52 | this.forJiang = original.forJiang; 53 | this.forShunkeCount = original.forShunkeCount; 54 | newUnits.units().forEach(unit -> { 55 | if (unit.type().isJiang()) { 56 | if (!this.forJiang) 57 | throw new RuntimeException("Redundant JIANG unit."); 58 | this.forJiang = false; 59 | } else { 60 | if (this.forShunkeCount <= 0) 61 | throw new RuntimeException("Redundant SHUNKE unit."); 62 | this.forShunkeCount--; 63 | } 64 | }); 65 | } 66 | 67 | /** 68 | * 从剩余的units中挑选尽量多指定类型的units,对所有可能的选择生成补充后的TileUnits并返回Stream。
    69 | * 生成的都是新实例,不改变当前实例。 70 | * 71 | * @param type 72 | * 挑选的unit类型 73 | * @param includeEmpty 74 | * 是否一定包含不补充任何unit,false表示只有没有unit可选或不需要补充时才包含不补充任何unit 75 | * @return 新TileUnits的Stream 76 | */ 77 | public Stream newToChoose(BazBotTileUnitType type, boolean includeEmpty) { 78 | if (type.isJiang()) { 79 | if (!forJiang) 80 | return Stream.of(this); 81 | } else { 82 | if (forShunkeCount == 0) 83 | return Stream.of(this); 84 | } 85 | 86 | // 在所有与当前所选不冲突的units中得出符合缺数的所有unit组合 87 | List unitCombs = nonConflictsInHoods(type).allCombs(type.isJiang() ? 1 : forShunkeCount, 88 | includeEmpty); 89 | 90 | // 选不出来则直接返回当前units 91 | if (unitCombs.isEmpty()) 92 | return Stream.of(this); 93 | 94 | // 复制当前TileUnits并拼接 95 | return unitCombs.stream().map(comb -> new BazBotChoosingTileUnits(this, comb)); 96 | } 97 | 98 | /** 99 | * 计算从当前选择的units到和牌所需的牌型列表,把所有可能的牌型列表组成Stream并返回。不需要内部排序和去重。 100 | */ 101 | public Stream> tileTypesToWin() { 102 | MutableObject>> resStream = new MutableObject<>(Stream.of(new ArrayList<>())); 103 | 104 | forEachHoodAndUnits((hood, units) -> { 105 | // 当前hood丢弃的牌 106 | Set remainingTiles = new HashSet<>(hood.getRemainingTiles(units)); 107 | units.forEach(unit -> { 108 | // 当前unit的(与丢弃的牌不冲突的)多组期望牌型 109 | List> newTypeLists = unit.forTileTypes(remainingTiles); 110 | // stream中所有牌型列表复制、拼接当前unit的各组期望牌型 111 | resStream.setValue(resStream.getValue() // 112 | .flatMap(typeList -> newTypeLists.stream() // 113 | .map(newTypeList -> merged(ArrayList::new, typeList, newTypeList)))); 114 | }); 115 | }); 116 | 117 | return resStream.getValue();// .peek(types -> System.out.println("units: " + this + "\n" + "for types: " + 118 | // types)); 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/bazbot/BazBotTileUnit.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.bazbot; 2 | 3 | import static java.util.Collections.*; 4 | import static java.util.stream.Collectors.*; 5 | 6 | import java.util.Collection; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | import com.github.blovemaple.mj.object.StandardTileUnitType; 11 | import com.github.blovemaple.mj.object.Tile; 12 | import com.github.blovemaple.mj.object.TileType; 13 | 14 | /** 15 | * @author blovemaple 16 | */ 17 | class BazBotTileUnit { 18 | private boolean completed; 19 | private StandardTileUnitType unitType; 20 | private Set tiles; 21 | 22 | private BazBotTileUnitType type; 23 | private BazBotTileNeighborhood hood; 24 | 25 | enum BazBotTileUnitType { 26 | /** 27 | * 完整将牌。 28 | */ 29 | COMPLETE_JIANG(true), 30 | /** 31 | * 完整顺刻。 32 | */ 33 | COMPLETE_SHUNKE(false), 34 | /** 35 | * 缺一张的不完整顺刻。 36 | */ 37 | UNCOMPLETE_SHUNKE_FOR_ONE(false), 38 | /** 39 | * 缺两张的不完整顺刻。 40 | */ 41 | UNCOMPLETE_SHUNKE_FOR_TWO(false), 42 | /** 43 | * 不完整将牌。 44 | */ 45 | UNCOMPLETE_JIANG(true); 46 | 47 | private final boolean isJiang; 48 | 49 | private BazBotTileUnitType(boolean isJiang) { 50 | this.isJiang = isJiang; 51 | } 52 | 53 | public boolean isJiang() { 54 | return isJiang; 55 | } 56 | 57 | public static BazBotTileUnitType of(boolean completed, StandardTileUnitType unitType, Set tiles) { 58 | switch (unitType) { 59 | case GANGZI: 60 | case HUA_UNIT: 61 | throw new RuntimeException("Unsupported StandardTileUnitType: " + unitType); 62 | case JIANG: 63 | return completed ? COMPLETE_JIANG : UNCOMPLETE_JIANG; 64 | case KEZI: 65 | case SHUNZI: 66 | return completed ? COMPLETE_SHUNKE 67 | : unitType.size() - tiles.size() == 1 ? UNCOMPLETE_SHUNKE_FOR_ONE : UNCOMPLETE_SHUNKE_FOR_TWO; 68 | default: 69 | throw new RuntimeException("Unrecognized StandardTileUnitType: " + unitType); 70 | } 71 | } 72 | 73 | } 74 | 75 | public static BazBotTileUnit completed(StandardTileUnitType unitType, Set tiles, 76 | BazBotTileNeighborhood hood) { 77 | return new BazBotTileUnit(true, unitType, tiles, hood); 78 | } 79 | 80 | public static BazBotTileUnit uncompleted(StandardTileUnitType unitType, Set tiles, 81 | BazBotTileNeighborhood hood) { 82 | return new BazBotTileUnit(false, unitType, tiles, hood); 83 | } 84 | 85 | private BazBotTileUnit(boolean isCompleted, StandardTileUnitType unitType, Set tiles, 86 | BazBotTileNeighborhood hood) { 87 | this.completed = isCompleted; 88 | this.unitType = unitType; 89 | this.tiles = tiles; 90 | this.type = BazBotTileUnitType.of(isCompleted, unitType, tiles); 91 | this.hood = hood; 92 | } 93 | 94 | public Set tiles() { 95 | return tiles; 96 | } 97 | 98 | public BazBotTileUnitType type() { 99 | return type; 100 | } 101 | 102 | public BazBotTileNeighborhood hood() { 103 | return hood; 104 | } 105 | 106 | public boolean conflictWith(BazBotTileUnit other) { 107 | if (tiles.isEmpty() || other.tiles.isEmpty()) 108 | return false; 109 | if (this == other) 110 | return !tiles.isEmpty(); 111 | return !disjoint(tiles, other.tiles); 112 | } 113 | 114 | public boolean conflictWith(Collection tiles) { 115 | if (this.tiles.isEmpty() || tiles.isEmpty()) 116 | return false; 117 | return !disjoint(this.tiles, tiles); 118 | } 119 | 120 | public List> forTileTypes(Set conflictTiles) { 121 | if (completed) 122 | return List.of(List.of()); 123 | else { 124 | Set conflictTileTypes = conflictTiles.stream().map(Tile::type).collect(toSet()); 125 | return unitType.getLackedTypesForTiles(this.tiles).stream() 126 | .filter(tileTypes -> disjoint(tileTypes, conflictTileTypes)).collect(toList()); 127 | } 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return "{" + (completed ? "" : "UC_") + unitType + ":" + tiles + "}"; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/WinActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.action.standard.AutoActionTypes.*; 4 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 5 | import static com.github.blovemaple.mj.utils.MyUtils.*; 6 | 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.function.BiPredicate; 10 | 11 | import com.github.blovemaple.mj.action.AbstractPlayerActionType; 12 | import com.github.blovemaple.mj.action.Action; 13 | import com.github.blovemaple.mj.action.IllegalActionException; 14 | import com.github.blovemaple.mj.action.PlayerAction; 15 | import com.github.blovemaple.mj.game.GameContext; 16 | import com.github.blovemaple.mj.game.GameContextPlayerView; 17 | import com.github.blovemaple.mj.game.GameResult; 18 | import com.github.blovemaple.mj.object.PlayerLocation; 19 | import com.github.blovemaple.mj.object.Tile; 20 | import com.github.blovemaple.mj.rule.win.FanType; 21 | import com.github.blovemaple.mj.rule.win.WinInfo; 22 | 23 | /** 24 | * 动作类型“和牌”。 25 | * 26 | * @author blovemaple 27 | */ 28 | public class WinActionType extends AbstractPlayerActionType { 29 | 30 | protected WinActionType() { 31 | } 32 | 33 | @Override 34 | public boolean canPass(GameContext context, PlayerLocation location) { 35 | return true; 36 | } 37 | 38 | @Override 39 | protected BiPredicate getLastActionPrecondition() { 40 | // 必须是发牌、自己摸牌,或别人打牌后 41 | return (a, location) -> DEAL.matchBy(a.getType()) || // 42 | (a instanceof PlayerAction && // 43 | (((PlayerAction) a).getLocation() == location ? // 44 | DRAW.matchBy(a.getType()) : DISCARD.matchBy(a.getType()))); 45 | } 46 | 47 | @Override 48 | protected int getActionTilesSize() { 49 | return 0; 50 | } 51 | 52 | @Override 53 | public boolean isLegalActionWithPreconition(GameContextPlayerView context, Set tiles) { 54 | Action lastAction = context.getLastAction(); 55 | Tile winTile = lastAction instanceof PlayerAction ? ((PlayerAction) lastAction).getTile() : null; 56 | boolean ziMo = !DISCARD.matchBy(context.getLastAction().getType()); 57 | WinInfo winInfo = WinInfo.fromPlayerTiles(context.getMyInfo(), winTile, ziMo); 58 | winInfo.setContextView(context); 59 | if (!ziMo) 60 | winInfo.setAliveTiles(mergedSet(context.getMyInfo().getAliveTiles(), winTile)); 61 | return context.getGameStrategy().canWin(winInfo); 62 | } 63 | 64 | @Override 65 | // XXX - 为了避免验证legal和算番时重复判断和牌,doAction时不进行legal验证,需要此方法的调用方保证legal(目前已保证)。 66 | public void doAction(GameContext context, Action action) throws IllegalActionException { 67 | Tile winTile = ((PlayerAction) context.getLastAction()).getTile(); 68 | boolean ziMo = !DISCARD.matchBy(context.getLastAction().getType()); 69 | PlayerLocation location = ((PlayerAction) action).getLocation(); 70 | 71 | GameResult result = new GameResult(context.getTable().getPlayerInfos(), context.getZhuangLocation()); 72 | result.setWinnerLocation(location); 73 | if (ziMo) { 74 | result.setWinTile(context.getPlayerView(location).getJustDrawedTile()); 75 | } else { 76 | result.setWinTile(winTile); 77 | result.setPaoerLocation(context.getLastActionLocation()); 78 | } 79 | 80 | // 和牌parse units、算番 81 | WinInfo winInfo = WinInfo.fromPlayerTiles(context.getPlayerInfoByLocation(location), winTile, ziMo); 82 | winInfo.setContextView(context.getPlayerView(location)); 83 | if (!ziMo) 84 | winInfo.setAliveTiles(mergedSet(context.getPlayerInfoByLocation(location).getAliveTiles(), winTile)); 85 | Map fans = context.getGameStrategy().getFans(winInfo); 86 | if (fans.isEmpty() && (winInfo.getUnits() == null || winInfo.getUnits().isEmpty())) 87 | throw new IllegalActionException(context, action); 88 | result.setFans(fans); 89 | 90 | context.setGameResult(result); 91 | } 92 | 93 | @Override 94 | protected void doLegalAction(GameContext context, PlayerLocation location, Set tiles) { 95 | throw new UnsupportedOperationException(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/game/GameContextImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.game; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.logging.Logger; 8 | 9 | import com.github.blovemaple.mj.action.Action; 10 | import com.github.blovemaple.mj.action.PlayerAction; 11 | import com.github.blovemaple.mj.object.MahjongTable; 12 | import com.github.blovemaple.mj.object.PlayerInfo; 13 | import com.github.blovemaple.mj.object.PlayerLocation; 14 | import com.github.blovemaple.mj.rule.GameStage; 15 | import com.github.blovemaple.mj.rule.GameStrategy; 16 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 17 | 18 | /** 19 | * {@link GameContext}实现。 20 | * 21 | * @author blovemaple 22 | */ 23 | public class GameContextImpl implements GameContext { 24 | @SuppressWarnings("unused") 25 | private static final Logger logger = Logger 26 | .getLogger(GameContextImpl.class.getSimpleName()); 27 | 28 | private MahjongTable table; 29 | private GameStrategy gameStrategy; 30 | private TimeLimitStrategy timeLimitStrategy; 31 | 32 | private PlayerLocation zhuangLocation; 33 | private GameStage stage; 34 | private List doneActions = new ArrayList<>(); 35 | private GameResult gameResult; 36 | 37 | public GameContextImpl(MahjongTable table, GameStrategy gameStrategy, TimeLimitStrategy timeLimitStrategy) { 38 | this.table = table; 39 | this.gameStrategy = gameStrategy; 40 | this.timeLimitStrategy = timeLimitStrategy; 41 | } 42 | 43 | @Override 44 | public MahjongTable getTable() { 45 | return table; 46 | } 47 | 48 | @Override 49 | public GameStrategy getGameStrategy() { 50 | return gameStrategy; 51 | } 52 | 53 | @Override 54 | public TimeLimitStrategy getTimeLimitStrategy() { 55 | return timeLimitStrategy; 56 | } 57 | 58 | @Override 59 | public PlayerInfo getPlayerInfoByLocation(PlayerLocation location) { 60 | return table.getPlayerInfos().get(location); 61 | } 62 | 63 | @Override 64 | public PlayerLocation getZhuangLocation() { 65 | return zhuangLocation; 66 | } 67 | 68 | @Override 69 | public void setZhuangLocation(PlayerLocation zhuangLocation) { 70 | this.zhuangLocation = zhuangLocation; 71 | } 72 | 73 | @Override 74 | public GameStage getStage() { 75 | return stage; 76 | } 77 | 78 | @Override 79 | public void setStage(GameStage stage) { 80 | this.stage = stage; 81 | } 82 | 83 | @Override 84 | public void actionDone(Action action) { 85 | doneActions.add(action); 86 | } 87 | 88 | @Override 89 | public Action getLastAction() { 90 | return doneActions.isEmpty() ? null 91 | : doneActions.get(doneActions.size() - 1); 92 | } 93 | 94 | @Override 95 | public PlayerLocation getLastActionLocation() { 96 | Action lastAction = getLastAction(); 97 | if (lastAction == null || !(lastAction instanceof PlayerAction)) 98 | return null; 99 | return ((PlayerAction) lastAction).getLocation(); 100 | } 101 | 102 | @Override 103 | public List getDoneActions() { 104 | return doneActions; 105 | } 106 | 107 | protected void setDoneActions(List doneActions) { 108 | this.doneActions = doneActions; 109 | } 110 | 111 | @Override 112 | public GameResult getGameResult() { 113 | return gameResult; 114 | } 115 | 116 | @Override 117 | public void setGameResult(GameResult gameResult) { 118 | this.gameResult = gameResult; 119 | } 120 | 121 | private final Map playerViews = new HashMap<>(); 122 | 123 | 124 | @Override 125 | public GameContextPlayerView getPlayerView(PlayerLocation location) { 126 | GameContextPlayerView view = playerViews.get(location); 127 | if (view == null) { // 不需要加锁,因为多创建了也没事 128 | view = newPlayerView(location); 129 | playerViews.put(location, view); 130 | } 131 | return view; 132 | } 133 | 134 | protected GameContextPlayerView newPlayerView(PlayerLocation location) { 135 | return new GameContextPlayerViewImpl(this, location); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/win/WinInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule.win; 2 | 3 | import java.util.Collection; 4 | import java.util.HashMap; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | import com.github.blovemaple.mj.game.GameContextPlayerView; 13 | import com.github.blovemaple.mj.object.PlayerTiles; 14 | import com.github.blovemaple.mj.object.Tile; 15 | import com.github.blovemaple.mj.object.TileGroup; 16 | import com.github.blovemaple.mj.object.TileType; 17 | import com.github.blovemaple.mj.object.TileUnit; 18 | 19 | /** 20 | * TODO comment 21 | * 22 | * @author blovemaple 23 | */ 24 | public class WinInfo extends PlayerTiles { 25 | /** 26 | * 从PlayerTiles及额外信息组建WinInfo。 27 | * 28 | * @param playerTiles 29 | * 玩家的牌,必须 30 | * @param winTile 31 | * 和牌时得到的牌,可选 32 | * @param ziMo 33 | * 是否自摸,可选 34 | * @return WinInfo对象 35 | */ 36 | public static WinInfo fromPlayerTiles(PlayerTiles playerTiles, Tile winTile, Boolean ziMo) { 37 | WinInfo winInfo = new WinInfo(); 38 | winInfo.setAliveTiles(playerTiles.getAliveTiles()); 39 | winInfo.setTileGroups(playerTiles.getTileGroups()); 40 | winInfo.setWinTile(winTile); 41 | winInfo.setZiMo(ziMo); 42 | return winInfo; 43 | } 44 | 45 | // 基类PlayerTiles的字段必须有 46 | // 以下三个字段是选填的额外信息,某些和牌类型和特殊的番种才可能会用到 47 | private Tile winTile; 48 | private Boolean ziMo; 49 | private GameContextPlayerView contextView; 50 | 51 | // 玩家手中所有牌,排序之后的。调用getTileTypes()时自动填入。 52 | private List tileTypes; 53 | 54 | // 检查WinType和FanType的时候填入的结果,WinType解析的units和FanType计入次数,用于: 55 | // (1)检查FanType时利用WinType的parse结果 56 | // (2)检查前先看是否已经有结果,避免重复检查 57 | private final Map>> units = new HashMap<>(); 58 | private final Map fans = new HashMap<>(); 59 | 60 | /** 61 | * 这个很丑的东西是给幺九刻准备的,
    62 | * 因为有几个番种有特殊规定,算了番的刻子不能再算幺九刻,所以把算了番的刻子都记录在这里,在判断幺九刻时排除这些刻子。 63 | */ 64 | private final Set noYaoJiuKeUnits = new HashSet<>(); 65 | 66 | public Tile getWinTile() { 67 | return winTile; 68 | } 69 | 70 | public void setWinTile(Tile winTile) { 71 | this.winTile = winTile; 72 | } 73 | 74 | public Boolean getZiMo() { 75 | return ziMo; 76 | } 77 | 78 | public void setZiMo(Boolean ziMo) { 79 | this.ziMo = ziMo; 80 | } 81 | 82 | public void setContextView(GameContextPlayerView contextView) { 83 | this.contextView = contextView; 84 | } 85 | 86 | public GameContextPlayerView getContextView() { 87 | return contextView; 88 | } 89 | 90 | public List getTileTypes() { 91 | if (tileTypes == null) { 92 | Stream tiles = getAliveTiles().stream(); 93 | for (TileGroup group : getTileGroups()) 94 | tiles = Stream.concat(tiles, group.getTiles().stream()); 95 | tileTypes = tiles.map(Tile::type).sorted().collect(Collectors.toList()); 96 | } 97 | return tileTypes; 98 | } 99 | 100 | public void setTileTypes(List tileTypes) { 101 | this.tileTypes = tileTypes; 102 | } 103 | 104 | public Map>> getUnits() { 105 | return units; 106 | } 107 | 108 | public void setUnits(WinType winType, List> units) { 109 | this.units.put(winType, units); 110 | } 111 | 112 | public Map getFans() { 113 | return fans; 114 | } 115 | 116 | public void setFans(FanType fanType, Integer fans) { 117 | this.fans.put(fanType, fans); 118 | } 119 | 120 | public Set getNoYaoJiuKeUnits() { 121 | return noYaoJiuKeUnits; 122 | } 123 | 124 | public void addNoYaoJiuKeUnits(Collection units) { 125 | noYaoJiuKeUnits.addAll(units); 126 | } 127 | 128 | @Override 129 | public String toString() { 130 | return "WinInfo [\nwinTile=" + winTile + ",\nziMo=" + ziMo + ",\ncontextView=" + contextView + ",\naliveTiles=" 131 | + aliveTiles + ",\ntileGroups=" + tileGroups + "\n]\n"; 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/botcompetition/BotCompetition.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.botcompetition; 2 | 3 | import static com.github.blovemaple.mj.object.PlayerLocation.*; 4 | 5 | import java.util.List; 6 | import java.util.function.Supplier; 7 | import java.util.logging.LogManager; 8 | 9 | import com.github.blovemaple.mj.cli.CliRunner; 10 | import com.github.blovemaple.mj.game.GameResult; 11 | import com.github.blovemaple.mj.game.MahjongGame; 12 | import com.github.blovemaple.mj.local.AbstractBot; 13 | import com.github.blovemaple.mj.local.bazbot.BazBot; 14 | import com.github.blovemaple.mj.object.MahjongTable; 15 | import com.github.blovemaple.mj.object.Player; 16 | import com.github.blovemaple.mj.rule.GameStrategy; 17 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 18 | import com.github.blovemaple.mj.rule.simple.SimpleGameStrategy; 19 | 20 | /** 21 | * @author blovemaple 22 | */ 23 | public class BotCompetition { 24 | public static void main(String[] args) { 25 | new BotCompetition(BazBot::new, BazBot::new).compete(1000); 26 | } 27 | 28 | private Supplier botSupplier1, botSupplier2; 29 | 30 | private GameStrategy gameStrategy = new SimpleGameStrategy(); 31 | private TimeLimitStrategy timeStrategy = TimeLimitStrategy.NO_LIMIT; 32 | 33 | public BotCompetition(Supplier botSupplier1, Supplier botSupplier2) { 34 | this.botSupplier1 = botSupplier1; 35 | this.botSupplier2 = botSupplier2; 36 | } 37 | 38 | // 序号,获胜的bot,坐庄的bot,耗时1,调用次数1,耗时2,调用次数2,本局总耗时 39 | public void compete(int gameCount) { 40 | try { 41 | LogManager.getLogManager() 42 | .readConfiguration(CliRunner.class.getResource("/logging_botcompetition.properties").openStream()); 43 | 44 | AbstractBot bot11 = botSupplier1.get(); 45 | AbstractBot bot12 = botSupplier1.get(); 46 | AbstractBot bot21 = botSupplier2.get(); 47 | AbstractBot bot22 = botSupplier2.get(); 48 | 49 | for (int gameIndex = 1; gameCount >= 0 && gameIndex <= gameCount; gameIndex++) { 50 | bot11.resetCostStat(); 51 | bot12.resetCostStat(); 52 | bot21.resetCostStat(); 53 | bot22.resetCostStat(); 54 | 55 | long startTime = System.nanoTime(); 56 | GameResult result; 57 | if (gameIndex % 2 == 1) 58 | result = compete(bot11, bot21, bot12, bot22); 59 | else 60 | result = compete(bot21, bot11, bot22, bot12); 61 | long gameCost = System.nanoTime() - startTime; 62 | 63 | AbstractBot winner = null; 64 | if (result.getWinnerLocation() != null) 65 | winner = (AbstractBot) result.getPlayerInfos().get(result.getWinnerLocation()).getPlayer(); 66 | AbstractBot zhuang = (AbstractBot) result.getPlayerInfos().get(result.getZhuangLocation()).getPlayer(); 67 | long cost1 = bot11.getCostSum() + bot12.getCostSum(); 68 | int invoke1 = bot11.getInvokeCount() + bot12.getInvokeCount(); 69 | long cost2 = bot21.getCostSum() + bot22.getCostSum(); 70 | int invoke2 = bot21.getInvokeCount() + bot22.getInvokeCount(); 71 | 72 | List outputs = List.of( // 73 | Integer.toString(gameIndex), // 74 | winner == null ? "0" : winner == bot11 || winner == bot12 ? "1" : "2", // 75 | zhuang == bot11 || zhuang == bot12 ? "1" : "2", // 76 | Long.toString(Math.round(cost1 / 1_000_000D)), // 77 | Integer.toString(invoke1), // 78 | Long.toString(Math.round(cost2 / 1_000_000D)), // 79 | Integer.toString(invoke2), // 80 | Long.toString(Math.round(gameCost / 1_000_000D)) // 81 | ); 82 | 83 | System.out.println(String.join("\t", outputs)); 84 | } 85 | } catch (Exception e) { 86 | e.printStackTrace(); 87 | } 88 | } 89 | 90 | private GameResult compete(Player player1, Player player2, Player player3, Player player4) { 91 | try { 92 | MahjongTable table = new MahjongTable(); 93 | table.init(); 94 | table.setPlayer(EAST, player1); 95 | table.setPlayer(SOUTH, player2); 96 | table.setPlayer(WEST, player3); 97 | table.setPlayer(NORTH, player4); 98 | 99 | MahjongGame game = new MahjongGame(gameStrategy, timeStrategy); 100 | return game.play(table); 101 | } catch (InterruptedException e) { 102 | // not possible 103 | throw new RuntimeException(e); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/utils/LanguageManager.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.utils; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.ResourceBundle; 7 | 8 | import com.github.blovemaple.mj.action.ActionType; 9 | import com.github.blovemaple.mj.object.PlayerLocation; 10 | import com.github.blovemaple.mj.object.TileRank; 11 | import com.github.blovemaple.mj.object.TileSuit; 12 | import com.github.blovemaple.mj.rule.win.FanType; 13 | 14 | /** 15 | * 语言管理器,屏蔽对语言的设置。 16 | * 17 | * @author blovemaple 18 | */ 19 | public class LanguageManager { 20 | 21 | private LanguageManager() { 22 | } 23 | 24 | private static final ResourceBundle resource = ResourceBundle 25 | .getBundle("message"); 26 | 27 | @FunctionalInterface 28 | public static interface Message { 29 | String str(); 30 | } 31 | 32 | private static final String TILE_SUIT_PREFIX = "TILE_SUIT_"; 33 | private static final String TILE_RANK_PREFIX = "TILE_RANK_"; 34 | private static final String ACTION_TYPE_PREFIX = "ACTION_TYPE_"; 35 | private static final String PLAYER_LOCATION_PREFIX = "PLAYER_LOCATION_"; 36 | private static final String PLAYER_RELATION_PREFIX = "PLAYER_RELATION_"; 37 | private static final String FAN_TYPE_PREFIX = "FAN_TYPE_"; 38 | private static final Map messageCache = Collections 39 | .synchronizedMap(new HashMap<>()); 40 | 41 | public static String str(TileSuit suit) { 42 | return message(suit).str(); 43 | } 44 | 45 | public static String str(TileRank rank) { 46 | return message(rank).str(); 47 | } 48 | 49 | public static String str(ActionType actionType) { 50 | return message(actionType).str(); 51 | } 52 | 53 | public static String str(PlayerLocation location) { 54 | return message(location).str(); 55 | } 56 | 57 | public static String str(PlayerLocation.Relation relation) { 58 | return message(relation).str(); 59 | } 60 | 61 | public static String str(FanType fanType) { 62 | return message(fanType).str(); 63 | } 64 | 65 | public static String str(String str) { 66 | return str; 67 | } 68 | 69 | public static Message message(TileSuit suit) { 70 | return message(TILE_SUIT_PREFIX + suit.name()); 71 | } 72 | 73 | public static Message message(TileRank rank) { 74 | return message(TILE_RANK_PREFIX + rank.name()); 75 | } 76 | 77 | public static Message message(ActionType actionType) { 78 | return message(ACTION_TYPE_PREFIX + actionType.name()); 79 | } 80 | 81 | public static Message message(PlayerLocation location) { 82 | return message(PLAYER_LOCATION_PREFIX + location.name()); 83 | } 84 | 85 | public static Message message(PlayerLocation.Relation relation) { 86 | return message(PLAYER_RELATION_PREFIX + relation.name()); 87 | } 88 | 89 | public static Message message(FanType fanType) { 90 | return message(FAN_TYPE_PREFIX + fanType.name()); 91 | } 92 | 93 | private static Message message(String name) { 94 | Message message = messageCache.get(name); 95 | if (message == null) 96 | messageCache.put(name, message = () -> ofName(name)); 97 | return message; 98 | } 99 | 100 | private static String ofName(String name) { 101 | return resource.getString(name); 102 | } 103 | 104 | public static enum ExtraMessage implements Message { 105 | /** 106 | * 发牌结束的提示 107 | */ 108 | DEAL_DONE, 109 | /** 110 | * 庄家 111 | */ 112 | ZHUANG, 113 | /** 114 | * 听牌 115 | */ 116 | TING, 117 | /** 118 | * 空格键 119 | */ 120 | SPACE_KEY, 121 | /** 122 | * M键 123 | */ 124 | M_KEY, 125 | /** 126 | * H键 127 | */ 128 | H_KEY, 129 | /** 130 | * 斜杠(/)键 131 | */ 132 | SLASH_KEY, 133 | /** 134 | * 逗号和句号键 135 | */ 136 | COMMA_AND_PERIOD_KEY, 137 | /** 138 | * 移动到下一个或上一个选项 139 | */ 140 | MOVE_CHOICE, 141 | /** 142 | * 放弃选择 143 | */ 144 | PASS, 145 | /** 146 | * 自摸 147 | */ 148 | ZIMO, 149 | /** 150 | * 点炮 151 | */ 152 | DIANPAO, 153 | /** 154 | * 番的总计 155 | */ 156 | FAN_TOTLE, 157 | /** 158 | * 番的单位 159 | */ 160 | FAN, 161 | /** 162 | * 窗口太窄的提示 163 | */ 164 | WINDOW_TOO_NARROW, 165 | /** 166 | * 询问是否开始新游戏,Y/N 167 | */ 168 | NEW_GAME_QUESTION, 169 | /** 170 | * 左右移动的按键提示 171 | */ 172 | MOVE_TIP, 173 | /** 174 | * github地址提示 175 | */ 176 | GITHUB_TIP, 177 | ; 178 | @Override 179 | public String str() { 180 | return LanguageManager.ofName(name()); 181 | } 182 | 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/standard/CpgActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action.standard; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | import static com.github.blovemaple.mj.utils.MyUtils.*; 5 | 6 | import java.util.Collection; 7 | import java.util.Objects; 8 | import java.util.Set; 9 | import java.util.function.BiPredicate; 10 | import java.util.logging.Logger; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | import com.github.blovemaple.mj.action.AbstractPlayerActionType; 15 | import com.github.blovemaple.mj.action.Action; 16 | import com.github.blovemaple.mj.action.ActionType; 17 | import com.github.blovemaple.mj.action.PlayerAction; 18 | import com.github.blovemaple.mj.game.GameContext; 19 | import com.github.blovemaple.mj.game.GameContextPlayerView; 20 | import com.github.blovemaple.mj.object.PlayerInfo; 21 | import com.github.blovemaple.mj.object.PlayerLocation; 22 | import com.github.blovemaple.mj.object.PlayerLocation.Relation; 23 | import com.github.blovemaple.mj.object.Tile; 24 | import com.github.blovemaple.mj.object.TileGroup; 25 | import com.github.blovemaple.mj.object.TileGroupType; 26 | 27 | /** 28 | * 吃、碰、直杠动作类型的统一逻辑。
    29 | * 这类动作的共同点是: 30 | *
  • 都可以放弃; 31 | *
  • 前提条件都是别的玩家出牌后; 32 | *
  • 都是从特定关系的玩家的出牌中得牌,并组成一种group。 33 | * 34 | * @author blovemaple 35 | */ 36 | public class CpgActionType extends AbstractPlayerActionType { 37 | @SuppressWarnings("unused") 38 | private static final Logger logger = Logger 39 | .getLogger(CpgActionType.class.getSimpleName()); 40 | 41 | private TileGroupType groupType; 42 | private Collection lastActionRelations; 43 | 44 | /** 45 | * 新建实例。 46 | * 47 | * @param groupType 48 | * 组成的牌组类型 49 | * @param lastActionRelations 50 | * 限制上一个动作的玩家(出牌者)与当前玩家的位置关系 51 | */ 52 | protected CpgActionType(TileGroupType groupType, 53 | Collection lastActionRelations) { 54 | Objects.requireNonNull(groupType); 55 | this.groupType = groupType; 56 | this.lastActionRelations = lastActionRelations != null 57 | ? lastActionRelations 58 | : Stream.of(Relation.values()).filter(Relation::isOther) 59 | .collect(Collectors.toList()); 60 | } 61 | 62 | /** 63 | * 新建实例。上一个动作的玩家(出牌者)与当前玩家的位置关系是所有其他人。 64 | * 65 | * @param groupType 66 | * 组成的牌组类型 67 | */ 68 | protected CpgActionType(TileGroupType groupType) { 69 | this(groupType, null); 70 | } 71 | 72 | @Override 73 | public boolean canPass(GameContext context, PlayerLocation location) { 74 | return true; 75 | } 76 | 77 | @Override 78 | protected boolean isAllowedInTing() { 79 | return false; 80 | } 81 | 82 | @Override 83 | protected BiPredicate getLastActionPrecondition() { 84 | // 必须是指定关系的人出牌后 85 | return (a, location) -> DISCARD.matchBy(a.getType()) 86 | && lastActionRelations.contains(location.getRelationOf(((PlayerAction) a).getLocation())); 87 | } 88 | 89 | @Override 90 | protected int getActionTilesSize() { 91 | return groupType.size() - 1; 92 | } 93 | 94 | @Override 95 | protected boolean isLegalActionWithPreconition(GameContextPlayerView context, 96 | Set tiles) { 97 | Set testTiles = mergedSet(tiles, (Tile) ((PlayerAction) context.getLastAction()).getTile()); 98 | boolean legal = groupType.isLegalTiles(testTiles); 99 | return legal; 100 | } 101 | 102 | @Override 103 | protected void doLegalAction(GameContext context, PlayerLocation location, 104 | Set tiles) { 105 | PlayerInfo playerInfo = context.getPlayerInfoByLocation(location); 106 | 107 | playerInfo.getAliveTiles().removeAll(tiles); 108 | 109 | Tile gotTile = ((PlayerAction) context.getLastAction()).getTile(); 110 | TileGroup group = new TileGroup(groupType, gotTile, 111 | location.getRelationOf(context.getLastActionLocation()), 112 | mergedSet(tiles, gotTile)); 113 | playerInfo.getTileGroups().add(group); 114 | } 115 | 116 | /** 117 | * 如果此类与testType的真正类是从属关系,并且testType的groupType与此对象相同,则视为match。 118 | * 119 | * @see com.github.blovemaple.mj.action.AbstractPlayerActionType#matchBy(com.github.blovemaple.mj.action.ActionType) 120 | */ 121 | @Override 122 | public boolean matchBy(ActionType testType) { 123 | if (!CpgActionType.class.isAssignableFrom(testType.getRealTypeClass())) 124 | return false; 125 | if (!groupType.equals( 126 | ((CpgActionType) testType.getRealTypeObject()).groupType)) 127 | return false; 128 | return true; 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/barbot/BarBotSimContext.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.barbot; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.github.blovemaple.mj.action.Action; 7 | import com.github.blovemaple.mj.action.PlayerAction; 8 | import com.github.blovemaple.mj.game.GameContext; 9 | import com.github.blovemaple.mj.game.GameContextPlayerView; 10 | import com.github.blovemaple.mj.game.GameContextPlayerViewImpl; 11 | import com.github.blovemaple.mj.game.GameResult; 12 | import com.github.blovemaple.mj.object.MahjongTable; 13 | import com.github.blovemaple.mj.object.PlayerInfo; 14 | import com.github.blovemaple.mj.object.PlayerLocation; 15 | import com.github.blovemaple.mj.rule.GameStage; 16 | import com.github.blovemaple.mj.rule.GameStrategy; 17 | import com.github.blovemaple.mj.rule.TimeLimitStrategy; 18 | import com.github.blovemaple.mj.utils.MyUtils; 19 | 20 | /** 21 | * 模拟选择时执行动作使用的GameContext。因为是机器人任务使用,所以只能从GameContext.PlayerView构建, 22 | * 只允许获取PlayerView能看到的一些信息。 23 | * 24 | * @author blovemaple 25 | */ 26 | @Deprecated 27 | public class BarBotSimContext implements GameContext { 28 | private GameStrategy gameStrategy; 29 | private TimeLimitStrategy timeLimitStrategy; 30 | 31 | private GameContextPlayerView contextView; 32 | private Action lastAction; 33 | private PlayerInfo myInfo; 34 | 35 | public BarBotSimContext(GameContextPlayerView contextView, Action lastAction, PlayerInfo myInfo) { 36 | this.gameStrategy = contextView.getGameStrategy(); 37 | this.timeLimitStrategy = contextView.getTimeLimitStrategy(); 38 | this.contextView = contextView; 39 | this.lastAction = lastAction; 40 | this.myInfo = myInfo; 41 | } 42 | 43 | @Override 44 | public MahjongTable getTable() { 45 | throw new UnsupportedOperationException(); 46 | } 47 | 48 | @Override 49 | public GameStrategy getGameStrategy() { 50 | return gameStrategy; 51 | } 52 | 53 | @Override 54 | public TimeLimitStrategy getTimeLimitStrategy() { 55 | return timeLimitStrategy; 56 | } 57 | 58 | @Override 59 | public PlayerInfo getPlayerInfoByLocation(PlayerLocation location) { 60 | if (location == contextView.getMyLocation()) 61 | return myInfo; 62 | else 63 | throw new UnsupportedOperationException(); 64 | } 65 | 66 | @Override 67 | public PlayerLocation getZhuangLocation() { 68 | return contextView.getZhuangLocation(); 69 | } 70 | 71 | @Override 72 | public void setZhuangLocation(PlayerLocation zhuangLocation) { 73 | throw new UnsupportedOperationException(); 74 | } 75 | 76 | @Override 77 | public GameStage getStage() { 78 | throw new UnsupportedOperationException(); 79 | } 80 | 81 | @Override 82 | public void setStage(GameStage stage) { 83 | throw new UnsupportedOperationException(); 84 | } 85 | 86 | @Override 87 | public void actionDone(Action action) { 88 | throw new UnsupportedOperationException(); 89 | } 90 | 91 | @Override 92 | public Action getLastAction() { 93 | if (lastAction == null) 94 | return contextView.getLastAction(); 95 | else 96 | return lastAction; 97 | } 98 | 99 | @Override 100 | public PlayerLocation getLastActionLocation() { 101 | if (lastAction == null) 102 | return contextView.getLastActionLocation(); 103 | else { 104 | if (lastAction == null || !(lastAction instanceof PlayerAction)) 105 | return null; 106 | return ((PlayerAction) lastAction).getLocation(); 107 | } 108 | } 109 | 110 | private List doneActions; 111 | 112 | @Override 113 | public List getDoneActions() { 114 | if (doneActions == null) { 115 | if (lastAction == null) 116 | doneActions = contextView.getDoneActions(); 117 | else 118 | doneActions = MyUtils.merged(ArrayList::new, contextView.getDoneActions(), 119 | lastAction); 120 | } 121 | return doneActions; 122 | } 123 | 124 | @Override 125 | public GameResult getGameResult() { 126 | return contextView.getGameResult(); 127 | } 128 | 129 | @Override 130 | public void setGameResult(GameResult gameResult) { 131 | throw new UnsupportedOperationException(); 132 | } 133 | 134 | @Override 135 | public GameContextPlayerView getPlayerView(PlayerLocation location) { 136 | if (location == contextView.getMyLocation()) 137 | return new GameContextPlayerViewImpl(this, location); 138 | else 139 | throw new UnsupportedOperationException(); 140 | } 141 | 142 | @Override 143 | public String toString() { 144 | return "[last action=" + getLastAction() + ", alive tiles=" + myInfo.getAliveTiles() + "]"; 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/TileGroup.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.io.Serializable; 4 | import java.util.Arrays; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import com.github.blovemaple.mj.object.PlayerLocation.Relation; 9 | 10 | /** 11 | * 牌组,即玩家的牌中除活牌外的若干个组,通常是吃、碰、杠等动作形成。 12 | * 13 | * @author blovemaple 14 | */ 15 | public class TileGroup implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | private TileGroupType type; 19 | private Set tiles; 20 | private Relation fromRelation; 21 | private Tile gotTile; 22 | 23 | /** 24 | * 新建一个实例。 25 | * 26 | * @param type 27 | * 类型 28 | * @param gotTile 29 | * 得牌 30 | * @param fromRelation 31 | * 得牌来自于哪个关系的玩家 32 | * @param tiles 33 | * 牌组中的牌 34 | * @throws IllegalArgumentException 35 | * 不合法 36 | */ 37 | public TileGroup(TileGroupType type, Tile gotTile, Relation fromRelation, 38 | Set tiles) { 39 | if (!type.isLegalTiles(tiles)) 40 | throw new IllegalArgumentException("Illegal group tiles:[" + type 41 | + "]" + Arrays.asList(tiles)); 42 | 43 | this.type = type; 44 | this.gotTile = gotTile; 45 | this.fromRelation = fromRelation; 46 | this.tiles = new HashSet<>(tiles); 47 | } 48 | 49 | /** 50 | * 新建一个实例,没有从其他玩家得到的牌。 51 | * 52 | * @param type 53 | * 类型 54 | * @param tiles 55 | * 牌组中的牌 56 | * @throws IllegalArgumentException 57 | * 不合法 58 | */ 59 | public TileGroup(TileGroupType type, Set tiles) { 60 | this(type, null, null, tiles); 61 | } 62 | 63 | /** 64 | * 返回类型。 65 | * 66 | * @return 类型 67 | */ 68 | public TileGroupType getType() { 69 | return type; 70 | } 71 | 72 | /** 73 | * 返回牌组中所有牌。 74 | * 75 | * @return tiles 集合 76 | */ 77 | public Set getTiles() { 78 | return tiles; 79 | } 80 | 81 | /** 82 | * 返回得牌来自于哪个关系的玩家。 83 | * 84 | * @return 玩家位置 85 | */ 86 | public Relation getFromRelation() { 87 | return fromRelation; 88 | } 89 | 90 | /** 91 | * 返回得牌。 92 | * 93 | * @return 得牌 94 | */ 95 | public Tile getGotTile() { 96 | return gotTile; 97 | } 98 | 99 | private PlayerView view; 100 | 101 | /** 102 | * 返回其他玩家的视图。 103 | * 104 | * @return 视图 105 | */ 106 | public TileGroupPlayerView getOtherPlayerView() { 107 | if (view == null) 108 | view = new PlayerView(); 109 | return view; 110 | } 111 | 112 | private class PlayerView implements TileGroupPlayerView { 113 | 114 | @Override 115 | public TileGroupType getType() { 116 | return TileGroup.this.getType(); 117 | } 118 | 119 | @Override 120 | public Set getTiles() { 121 | if (getType() == TileGroupType.ANGANG_GROUP) 122 | return null; 123 | return TileGroup.this.getTiles(); 124 | } 125 | 126 | @Override 127 | public Relation getFromRelation() { 128 | return TileGroup.this.getFromRelation(); 129 | } 130 | 131 | @Override 132 | public Tile getGotTile() { 133 | if (getType() == TileGroupType.ANGANG_GROUP) 134 | return null; 135 | return TileGroup.this.getGotTile(); 136 | } 137 | 138 | } 139 | 140 | @Override 141 | public int hashCode() { 142 | final int prime = 31; 143 | int result = 1; 144 | result = prime * result + ((gotTile == null) ? 0 : gotTile.hashCode()); 145 | result = prime * result 146 | + ((fromRelation == null) ? 0 : fromRelation.hashCode()); 147 | result = prime * result + ((tiles == null) ? 0 : tiles.hashCode()); 148 | result = prime * result + ((type == null) ? 0 : type.hashCode()); 149 | return result; 150 | } 151 | 152 | @Override 153 | public boolean equals(Object obj) { 154 | if (this == obj) 155 | return true; 156 | if (obj == null) 157 | return false; 158 | if (!(obj instanceof TileGroup)) 159 | return false; 160 | TileGroup other = (TileGroup) obj; 161 | if (gotTile == null) { 162 | if (other.gotTile != null) 163 | return false; 164 | } else if (!gotTile.equals(other.gotTile)) 165 | return false; 166 | if (fromRelation == null) { 167 | if (other.fromRelation != null) 168 | return false; 169 | } else if (!fromRelation.equals(other.fromRelation)) 170 | return false; 171 | if (tiles == null) { 172 | if (other.tiles != null) 173 | return false; 174 | } else if (!tiles.equals(other.tiles)) 175 | return false; 176 | if (type != other.type) 177 | return false; 178 | return true; 179 | } 180 | 181 | /** 182 | * Just for debug. 183 | * 184 | * @see java.lang.Object#toString() 185 | */ 186 | @Override 187 | public String toString() { 188 | return "[" + type + " " + gotTile + " from " + fromRelation 189 | + " to compose " + tiles + "]"; 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/rule/AbstractGameStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.rule; 2 | 3 | import static com.github.blovemaple.mj.action.standard.AutoActionTypes.*; 4 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 5 | import static java.util.function.Function.*; 6 | import static java.util.stream.Collectors.*; 7 | 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.Comparator; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Objects; 14 | import java.util.Set; 15 | import java.util.stream.Stream; 16 | 17 | import com.github.blovemaple.mj.action.Action; 18 | import com.github.blovemaple.mj.action.ActionType; 19 | import com.github.blovemaple.mj.action.ActionTypeAndLocation; 20 | import com.github.blovemaple.mj.action.PlayerAction; 21 | import com.github.blovemaple.mj.action.PlayerActionType; 22 | import com.github.blovemaple.mj.game.GameContext; 23 | import com.github.blovemaple.mj.object.MahjongTable; 24 | import com.github.blovemaple.mj.object.PlayerLocation; 25 | import com.github.blovemaple.mj.object.PlayerLocation.Relation; 26 | import com.github.blovemaple.mj.object.Tile; 27 | 28 | /** 29 | * 基本的常用的规则。继承此类,实现抽象方法,可以实现一种特定的游戏规则,包括坐庄规则、和牌限制、和牌类型、番种定义等。 30 | * 31 | * @author blovemaple 32 | */ 33 | public abstract class AbstractGameStrategy implements GameStrategy { 34 | 35 | private Map stages; 36 | 37 | /** 38 | * {@inheritDoc}
    39 | * 桌上所有位置都有玩家就返回true。 40 | * 41 | * @see com.github.blovemaple.mj.rule.GameStrategy#checkReady(com.github.blovemaple.mj.object.MahjongTable) 42 | */ 43 | @Override 44 | public boolean checkReady(MahjongTable table) { 45 | return Stream.of(PlayerLocation.values()) 46 | .map(table::getPlayerByLocation).allMatch(Objects::nonNull); 47 | } 48 | 49 | @Override 50 | public List getAllTiles() { 51 | return Tile.all(); 52 | } 53 | 54 | @Override 55 | public GameStage getStageByName(String stageName) { 56 | if (stages == null) { 57 | synchronized (this) { 58 | if (stages == null) 59 | stages = getAllStages().stream().collect(toMap(GameStage::getName, identity())); 60 | } 61 | } 62 | return stages.get(stageName); 63 | } 64 | 65 | /** 66 | * 返回所有阶段对象。 67 | */ 68 | protected abstract Collection getAllStages(); 69 | 70 | /** 71 | * {@inheritDoc}
    72 | * 在一局开始之前设置庄家位置和初始阶段。 73 | * 74 | * @see com.github.blovemaple.mj.rule.GameStrategy#readyContext(com.github.blovemaple.mj.game.GameContext) 75 | */ 76 | @Override 77 | public void readyContext(GameContext context) { 78 | context.setZhuangLocation(nextZhuangLocation(context)); 79 | } 80 | 81 | /** 82 | * 在一局开始之前,根据context返回此局的庄家位置。 83 | */ 84 | protected abstract PlayerLocation nextZhuangLocation(GameContext context); 85 | 86 | /** 87 | * 动作类型优先级倒序,先低后高,不在列表里的为最低。
    88 | * 进行比较时反过来用index比较,不在列表里的为-1。 89 | */ 90 | private static final List ACTION_TYPE_PRIORITY_LIST = Arrays 91 | .asList(CHI, PENG, ZHIGANG, BUHUA, DRAW_BOTTOM, WIN); 92 | 93 | /** 94 | * 和>补花>杠>碰>吃>其他,相同的比较与上次动作的玩家位置关系。 95 | * 96 | * @see com.github.blovemaple.mj.rule.GameStrategy#getActionPriorityComparator() 97 | */ 98 | @Override 99 | public Comparator getActionPriorityComparator() { 100 | Comparator c = Comparator.comparing( 101 | atl -> ACTION_TYPE_PRIORITY_LIST.indexOf(atl.getActionType())); 102 | c = c.reversed(); 103 | c = c.thenComparing(a -> { 104 | PlayerLocation lastLocation = a.getContext() 105 | .getLastActionLocation(); 106 | return lastLocation == null || a.getLocation() == null ? Relation.SELF 107 | : lastLocation.getRelationOf(a.getLocation()); 108 | }); 109 | return c; 110 | } 111 | 112 | @Override 113 | public PlayerAction getPlayerDefaultAction(GameContext context, 114 | PlayerLocation location, Set choises) { 115 | if (choises.contains(DRAW)) 116 | return new PlayerAction(location, DRAW); 117 | if (choises.contains(DRAW_BOTTOM)) 118 | return new PlayerAction(location, DRAW_BOTTOM); 119 | if (choises.contains(DISCARD)) { 120 | Tile tileToDiscard; 121 | Action lastAction = context.getLastAction(); 122 | if (context.getLastActionLocation() == location 123 | && DRAW.matchBy(lastAction.getType())) { 124 | tileToDiscard = ((PlayerAction) lastAction).getTile(); 125 | } else { 126 | tileToDiscard = context.getPlayerInfoByLocation(location) 127 | .getAliveTiles().iterator().next(); 128 | } 129 | return new PlayerAction(location, DISCARD, tileToDiscard); 130 | } 131 | return null; 132 | } 133 | 134 | @Override 135 | public Action getDefaultAction(GameContext context, 136 | Map> choises) { 137 | if (context.getTable().getTileWallSize() == 0) 138 | return new Action(LIUJU); 139 | else 140 | return null; 141 | } 142 | 143 | @Override 144 | public boolean tryEndGame(GameContext context) { 145 | return context.getGameResult() != null; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/object/MahjongTable.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.object; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.EnumMap; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * 麻将桌。 14 | * 15 | * @author blovemaple 16 | */ 17 | public class MahjongTable { 18 | /** 19 | * 牌墙。从0处摸牌。一局开始时如果产生底牌,应该把底牌从开头挪到尾部。 20 | */ 21 | private List tileWall; 22 | /** 23 | * 一局开始时的底牌数量。 24 | */ 25 | private int initBottomSize; 26 | /** 27 | * 已经从底部摸牌的数量。 28 | */ 29 | private int drawedBottomSize; 30 | /** 31 | * 所有玩家信息。 32 | */ 33 | private Map playerInfos; 34 | 35 | public void init() { 36 | tileWall = new ArrayList(); 37 | playerInfos = new EnumMap<>(PlayerLocation.class); 38 | for (PlayerLocation location : PlayerLocation.values()) { 39 | playerInfos.put(location, new PlayerInfo()); 40 | } 41 | } 42 | 43 | /** 44 | * 初始化,准备开始一局。即清空玩家的牌、洗牌、摆牌墙。 45 | */ 46 | public void readyForGame(Collection allTiles) { 47 | playerInfos.values().forEach(PlayerInfo::clear); 48 | tileWall.clear(); 49 | tileWall.addAll(allTiles); 50 | Collections.shuffle(tileWall); 51 | initBottomSize = 0; 52 | drawedBottomSize = 0; 53 | } 54 | 55 | /** 56 | * 返回牌墙中的剩余牌数。 57 | */ 58 | public int getTileWallSize() { 59 | return tileWall.size(); 60 | } 61 | 62 | public int getInitBottomSize() { 63 | return initBottomSize; 64 | } 65 | 66 | public void setInitBottomSize(int initBottomSize) { 67 | this.initBottomSize = initBottomSize; 68 | } 69 | 70 | public int getDrawedBottomSize() { 71 | return drawedBottomSize; 72 | } 73 | 74 | public void setDrawedBottomSize(int drawedBottomSize) { 75 | this.drawedBottomSize = drawedBottomSize; 76 | } 77 | 78 | /** 79 | * 从牌墙的头部摸指定数量的牌并返回。 80 | */ 81 | public List draw(int count) { 82 | if (count <= 0 || count > tileWall.size()) 83 | return Collections.emptyList(); 84 | List toBeDrawed = tileWall.subList(0, count); 85 | List drawed = new ArrayList<>(toBeDrawed); 86 | toBeDrawed.clear(); 87 | return drawed; 88 | } 89 | 90 | /** 91 | * 从牌墙的底部摸指定数量的牌并返回。 92 | */ 93 | public List drawBottom(int count) { 94 | if (count <= 0 || count > tileWall.size()) 95 | return Collections.emptyList(); 96 | List toBeDrawed = tileWall.subList(tileWall.size() - count, 97 | tileWall.size()); 98 | List drawed = new ArrayList<>(toBeDrawed); 99 | toBeDrawed.clear(); 100 | drawedBottomSize += drawed.size(); 101 | return drawed; 102 | } 103 | 104 | public Map getPlayerInfos() { 105 | return playerInfos; 106 | } 107 | 108 | protected void setPlayerInfos(Map playerInfos) { 109 | this.playerInfos = playerInfos; 110 | } 111 | 112 | public Player getPlayerByLocation(PlayerLocation location) { 113 | PlayerInfo info = playerInfos.get(location); 114 | return info == null ? null : info.getPlayer(); 115 | } 116 | 117 | public void setPlayer(PlayerLocation location, Player player) { 118 | PlayerInfo playerInfo = playerInfos.get(location); 119 | if (playerInfo == null) { 120 | playerInfo = new PlayerInfo(); 121 | playerInfos.put(location, playerInfo); 122 | } 123 | playerInfo.setPlayer(player); 124 | } 125 | 126 | private final Map playerViews = new HashMap<>(); 127 | 128 | /** 129 | * 获取指定位置的玩家视图。 130 | */ 131 | public MahjongTablePlayerView getPlayerView(PlayerLocation location) { 132 | PlayerView view = playerViews.get(location); 133 | if (view == null) { // 不需要加锁,因为多创建了也没事 134 | view = new PlayerView(location); 135 | playerViews.put(location, view); 136 | } 137 | return view; 138 | } 139 | 140 | private class PlayerView implements MahjongTablePlayerView { 141 | 142 | private final PlayerLocation myLocation; 143 | 144 | private PlayerView(PlayerLocation myLocation) { 145 | this.myLocation = myLocation; 146 | } 147 | 148 | @Override 149 | public PlayerLocation getMyLocation() { 150 | return myLocation; 151 | } 152 | 153 | @Override 154 | public String getPlayerName(PlayerLocation location) { 155 | Player player = getPlayerByLocation(location); 156 | return player != null ? player.getName() : null; 157 | } 158 | 159 | @Override 160 | public int getTileWallSize() { 161 | return MahjongTable.this.getTileWallSize(); 162 | } 163 | 164 | @Override 165 | public int getInitBottomSize() { 166 | return MahjongTable.this.getInitBottomSize(); 167 | } 168 | 169 | @Override 170 | public int getDrawedBottomSize() { 171 | return MahjongTable.this.getDrawedBottomSize(); 172 | } 173 | 174 | @Override 175 | public Map getPlayerInfoView() { 176 | return playerInfos.entrySet().stream() 177 | .collect(Collectors.toMap(entry -> entry.getKey() 178 | , entry -> entry.getValue().getOtherPlayerView())); 179 | } 180 | 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/bazbot/BazBotTileUnits.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.bazbot; 2 | 3 | import static java.util.function.Function.*; 4 | import static java.util.stream.Collectors.*; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.LinkedHashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.function.BiConsumer; 12 | import java.util.stream.IntStream; 13 | import java.util.stream.Stream; 14 | 15 | import com.github.blovemaple.mj.local.bazbot.BazBotTileUnit.BazBotTileUnitType; 16 | 17 | /** 18 | * BazBotTileUnit组合。 19 | * 20 | * @author blovemaple 21 | */ 22 | class BazBotTileUnits { 23 | /** 24 | * neighborbood - unit列表。 25 | */ 26 | private Map> unitsByNeighborhood; 27 | 28 | private int size; 29 | 30 | public BazBotTileUnits(Collection neighborhoods) { 31 | unitsByNeighborhood = new LinkedHashMap<>(); 32 | neighborhoods.forEach(hood -> unitsByNeighborhood.put(hood, new ArrayList<>())); 33 | size = 0; 34 | } 35 | 36 | public BazBotTileUnits(BazBotTileUnits original, BazBotTileUnits newUnits) { 37 | // 拷贝original的unitsByNeighborhood 38 | this.unitsByNeighborhood = new LinkedHashMap<>(); 39 | original.unitsByNeighborhood 40 | .forEach((hood, units) -> this.unitsByNeighborhood.put(hood, new ArrayList<>(units))); 41 | 42 | // 把newUnits填入unitsByNeighborhood 43 | if (newUnits != null) 44 | newUnits.unitsByNeighborhood.forEach((hood, units) -> this.unitsByNeighborhood.get(hood).addAll(units)); 45 | 46 | size = original.size() + newUnits.size(); 47 | } 48 | 49 | private BazBotTileUnits(Map> unitsByNeighborhood) { 50 | this.unitsByNeighborhood = unitsByNeighborhood; 51 | size = unitsByNeighborhood.values().stream().mapToInt(List::size).sum(); 52 | } 53 | 54 | protected Collection neighborhoods() { 55 | return unitsByNeighborhood.keySet(); 56 | } 57 | 58 | public Stream units() { 59 | return unitsByNeighborhood.values().stream().flatMap(List::stream); 60 | } 61 | 62 | public void forEachHoodAndUnits(BiConsumer> action) { 63 | unitsByNeighborhood.forEach(action); 64 | } 65 | 66 | public List unitsOfHood(BazBotTileNeighborhood hood) { 67 | return unitsByNeighborhood.get(hood); 68 | } 69 | 70 | public BazBotTileUnits nonConflictsInHoods(BazBotTileUnitType type) { 71 | return new BazBotTileUnits( // 72 | neighborhoods().stream() 73 | .collect(toMap(identity(), hood -> hood.getNonConflictingUnits(type, unitsOfHood(hood)))) // 74 | ); 75 | } 76 | 77 | /** 78 | * 挑选符合数量要求的unit组合,返回所有的可能。
    79 | * 先找到第一个unit,然后返回以下结果: 80 | *
  • 把数量要求减1、去除与第一个unit冲突者,并递归调用,最后把第一个unit和递归调用的结果分别拼接; 81 | *
  • 按照原数量要求,从所有剩余unit中递归获取结果。 82 | * 83 | * @param maxUnitCount 84 | * 返回的组合中的最多unit数量 85 | * @param includeEmpty 86 | * 是否一定包含空组合,false表示只有没有unit可选或maxUnitCount==0时才包含空组合 87 | * @return 所有符合要求的unit组合列表 88 | */ 89 | public List allCombs(int maxUnitCount, boolean includeEmpty) { 90 | if (maxUnitCount <= 0) 91 | // 数量要求为0,选择一个空组合 92 | return List.of(new BazBotTileUnits(neighborhoods())); 93 | 94 | List allUnits = unitsByNeighborhood.values().stream().flatMap(List::stream).collect(toList()); 95 | if (allUnits.isEmpty()) 96 | // 没有unit可选 97 | return includeEmpty ? List.of(new BazBotTileUnits(neighborhoods())) : List.of(); 98 | 99 | List res = allCombs(allUnits, 0, List.of(), List.of(), true, maxUnitCount); 100 | 101 | if (includeEmpty) { 102 | res = new ArrayList<>(res); 103 | res.add(new BazBotTileUnits(neighborhoods())); 104 | } 105 | 106 | return res; 107 | } 108 | 109 | private List allCombs(List allUnits, int startIndex, 110 | List selecteds, List droppeds, boolean satisfiedWithDroppeds, 111 | int forUnitCount) { 112 | if (forUnitCount <= 0) 113 | // 数量要求为0,选择一个空组合 114 | return List.of(new BazBotTileUnits(neighborhoods())); 115 | if (allUnits.isEmpty() || startIndex >= allUnits.size()) 116 | // 没有unit可选 117 | // 当satisfiedWithDroppeds时选择一个空组合,否则不选择 118 | return satisfiedWithDroppeds ? List.of(new BazBotTileUnits(neighborhoods())) : List.of(); 119 | 120 | // 从startIndex开始,找到第一个与已选units不冲突的unit 121 | int chosenIndex = IntStream.range(startIndex, allUnits.size()) 122 | .dropWhile(index -> selecteds.stream().anyMatch(conflict -> conflict.conflictWith(allUnits.get(index)))) 123 | .findFirst().orElse(-1); 124 | if (chosenIndex < 0) 125 | // 没有unit可选(都冲突) 126 | // 当satisfiedWithDroppeds时选择一个空组合,否则不选择 127 | return satisfiedWithDroppeds ? List.of(new BazBotTileUnits(neighborhoods())) : List.of(); 128 | BazBotTileUnit chosenUnit = allUnits.get(chosenIndex); 129 | 130 | List res = new ArrayList<>(); 131 | 132 | // 选择此unit,递归调用并与此unit组合 133 | List crtSelecteds = new ArrayList<>(selecteds); 134 | crtSelecteds.add(chosenUnit); 135 | boolean crtSatisfiedWithDroppeds = satisfiedWithDroppeds ? true 136 | : droppeds.stream().allMatch(dropped -> dropped.conflictWith(chosenUnit)); 137 | if (!crtSatisfiedWithDroppeds) { 138 | if (allUnits.size() - chosenIndex < forUnitCount) 139 | return res; 140 | } 141 | allCombs(allUnits, chosenIndex + 1, crtSelecteds, droppeds, crtSatisfiedWithDroppeds, forUnitCount - 1).stream() 142 | .peek(units -> units.add(chosenUnit.hood(), chosenUnit)) // 143 | .filter(units -> crtSatisfiedWithDroppeds ? true : units.size() == forUnitCount) // 144 | .forEach(res::add); 145 | 146 | // 不选此unit,递归调用 147 | List crtDroppeds = new ArrayList<>(droppeds); 148 | crtDroppeds.add(chosenUnit); 149 | allCombs(allUnits, chosenIndex + 1, selecteds, crtDroppeds, false, forUnitCount).stream() 150 | .filter(units -> crtSatisfiedWithDroppeds ? true : units.size() == forUnitCount) // 151 | .forEach(res::add); 152 | 153 | return res; 154 | } 155 | 156 | private void add(BazBotTileNeighborhood hood, BazBotTileUnit newUnit) { 157 | unitsByNeighborhood.get(hood).add(newUnit); 158 | size += 1; 159 | } 160 | 161 | public int size() { 162 | return size; 163 | } 164 | 165 | @Override 166 | public String toString() { 167 | return units().collect(toList()).toString(); 168 | } 169 | } -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/AbstractBot.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local; 2 | 3 | import static com.github.blovemaple.mj.action.standard.PlayerActionTypes.*; 4 | import static com.github.blovemaple.mj.utils.MyUtils.*; 5 | import static java.util.stream.Collectors.*; 6 | 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.logging.Logger; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | 15 | import com.github.blovemaple.mj.action.Action; 16 | import com.github.blovemaple.mj.action.PlayerAction; 17 | import com.github.blovemaple.mj.action.PlayerActionType; 18 | import com.github.blovemaple.mj.cli.CliGameView; 19 | import com.github.blovemaple.mj.game.GameContextPlayerView; 20 | import com.github.blovemaple.mj.object.Player; 21 | import com.github.blovemaple.mj.object.Tile; 22 | 23 | /** 24 | * @author blovemaple 25 | */ 26 | public abstract class AbstractBot implements Player { 27 | private static final Logger logger = Logger.getLogger(AbstractBot.class.getSimpleName()); 28 | 29 | private String name; 30 | private int minThinkingMs, maxThinkingMs; 31 | 32 | private long costSum; 33 | private int invokeCount; 34 | 35 | public AbstractBot(String name) { 36 | this.name = name; 37 | } 38 | 39 | @Override 40 | public String getName() { 41 | return name; 42 | } 43 | 44 | public AbstractBot thinkingTime(int min, int max) { 45 | if (min > max) 46 | throw new IllegalArgumentException("Invalid thinking time: [" + min + "," + max + "]"); 47 | this.minThinkingMs = min; 48 | this.maxThinkingMs = max; 49 | return this; 50 | } 51 | 52 | public void resetCostStat() { 53 | costSum = 0; 54 | invokeCount = 0; 55 | } 56 | 57 | public long getCostSum() { 58 | return costSum; 59 | } 60 | 61 | public int getInvokeCount() { 62 | return invokeCount; 63 | } 64 | 65 | @Override 66 | public PlayerAction chooseAction(GameContextPlayerView contextView, Set actionTypes) 67 | throws InterruptedException { 68 | long startTime = System.nanoTime(); 69 | 70 | logger.info(() -> "BOT Alive tiles: " + aliveTilesStr(contextView)); 71 | PlayerAction action = chooseAction0(contextView, actionTypes); 72 | logger.info(() -> "BOT Chosed action:" + action); 73 | 74 | // 没到目标时间的话假装再想一会儿 75 | long endTime = System.nanoTime(); 76 | long nanoCost = endTime - startTime; 77 | int targetThinkingTime = minThinkingMs + (int) (Math.random() * (maxThinkingMs - minThinkingMs)); 78 | long delayMillis = targetThinkingTime - TimeUnit.MILLISECONDS.convert(nanoCost, TimeUnit.NANOSECONDS); 79 | TimeUnit.MILLISECONDS.sleep(delayMillis); 80 | return action; 81 | } 82 | 83 | private PlayerAction chooseAction0(GameContextPlayerView contextView, Set actionTypes) 84 | throws InterruptedException { 85 | // 如果可以和,就和 86 | if (actionTypes.contains(WIN)) 87 | return new PlayerAction(contextView.getMyLocation(), WIN); 88 | 89 | // 如果可以摸底,就摸底 90 | if (actionTypes.contains(DRAW_BOTTOM)) 91 | return new PlayerAction(contextView.getMyLocation(), DRAW_BOTTOM); 92 | 93 | // 如果可以补花,就补花 94 | if (actionTypes.contains(BUHUA)) { 95 | Collection> buhuas = BUHUA.getLegalActionTiles(contextView); 96 | if (!buhuas.isEmpty()) { 97 | return new PlayerAction(contextView.getMyLocation(), BUHUA, buhuas.iterator().next()); 98 | } 99 | } 100 | 101 | // 如果可以吃/碰/杠/出牌,就选择 102 | List cpgdActions = Stream 103 | .concat(cpgdActions(contextView, actionTypes), passAction(contextView)).collect(toList()); 104 | PlayerAction action = cpgdActions.isEmpty() ? null 105 | : cpgdActions.size() == 1 ? cpgdActions.get(0) 106 | : chooseCpgdActionWithTimer(contextView, actionTypes, cpgdActions); 107 | if (action != null) 108 | return action; 109 | 110 | // 如果可以摸牌,就摸牌 111 | if (actionTypes.contains(DRAW)) 112 | return new PlayerAction(contextView.getMyLocation(), DRAW); 113 | 114 | // 啥都没选择,放弃了 115 | return null; 116 | } 117 | 118 | private String aliveTilesStr(GameContextPlayerView contextView) { 119 | StringBuilder aliveTilesStr = new StringBuilder(); 120 | Tile justDrawed = contextView.getMyInfo().getLastDrawedTile(); 121 | CliGameView.appendAliveTiles(aliveTilesStr, contextView.getMyInfo().getAliveTiles(), null, 122 | justDrawed != null ? Set.of(contextView.getMyInfo().getLastDrawedTile()) : null); 123 | return aliveTilesStr.toString(); 124 | } 125 | 126 | private Stream cpgdActions(GameContextPlayerView contextView, Set actionTypes) { 127 | return 128 | // 吃/碰/杠/出牌动作类型 129 | Stream.of(CHI, PENG, ZHIGANG, BUGANG, ANGANG, DISCARD, DISCARD_WITH_TING) 130 | // 过滤出actionTypes有的 131 | .filter(actionTypes::contains) 132 | // 生成合法动作,并按照牌型去重 133 | .flatMap(actionType -> actionType 134 | // 生成合法tiles 135 | .getLegalActionTiles(contextView).stream() 136 | // 按牌型去重 137 | .filter(distinctorBy( 138 | tiles -> tiles.stream().map(Tile::type).sorted().collect(Collectors.toList()))) 139 | // 构造Action 140 | .map(tiles -> new PlayerAction(contextView.getMyLocation(), actionType, tiles))); 141 | } 142 | 143 | private Stream passAction(GameContextPlayerView contextView) { 144 | if (contextView.getMyInfo().getAliveTiles().size() % 3 == 1) 145 | return Stream.of((PlayerAction) null); 146 | else 147 | return Stream.empty(); 148 | } 149 | 150 | private PlayerAction chooseCpgdActionWithTimer(GameContextPlayerView contextView, Set actionTypes, 151 | List actions) throws InterruptedException { 152 | long startTime = System.nanoTime(); 153 | try { 154 | return chooseCpgdAction(contextView, actionTypes, actions); 155 | } finally { 156 | long endTime = System.nanoTime(); 157 | long nanoCost = endTime - startTime; 158 | costSum += nanoCost; 159 | invokeCount++; 160 | logger.info("BOT Time cost(millis): " + Math.round(nanoCost / 1_000_000D)); 161 | } 162 | } 163 | 164 | protected abstract PlayerAction chooseCpgdAction(GameContextPlayerView contextView, 165 | Set actionTypes, List actions) throws InterruptedException; 166 | 167 | @Override 168 | public PlayerAction chooseAction(GameContextPlayerView contextView, Set actionTypes, 169 | PlayerAction illegalAction) throws InterruptedException { 170 | logger.severe("Selected illegal action: " + illegalAction); 171 | return null; 172 | } 173 | 174 | @Override 175 | public void actionDone(GameContextPlayerView contextView, Action action) { 176 | } 177 | 178 | @Override 179 | public void timeLimit(GameContextPlayerView contextView, Integer secondsToGo) { 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/cli/CliView.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.cli; 2 | 3 | import static com.github.blovemaple.mj.utils.LanguageManager.ExtraMessage.*; 4 | import static com.github.blovemaple.mj.utils.MyUtils.*; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.PrintStream; 9 | import java.lang.Thread.State; 10 | import java.util.List; 11 | import java.util.concurrent.CopyOnWriteArrayList; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | 15 | import org.jline.terminal.Terminal; 16 | import org.jline.terminal.TerminalBuilder; 17 | 18 | /** 19 | * 命令行界面。提供信息显示以及最下方的状态栏显示,以及接受用户单字符无回显输入。
    20 | * 利用jline-terminal关闭回显,并进入raw模式。 21 | * 22 | * @author blovemaple 23 | */ 24 | class CliView { 25 | private static final Logger logger = Logger 26 | .getLogger(CliView.class.getSimpleName()); 27 | 28 | private final Terminal terminal; 29 | private final InputStream in; 30 | private final PrintStream out; 31 | 32 | // 分割线字符 33 | private static final char SPLIT_LINE_CHAR = '*'; 34 | 35 | private String status = ""; 36 | private List charHandlers = new CopyOnWriteArrayList<>(); 37 | private CharHandler monoHandler; // 目前独占字符处理的处理器 38 | 39 | /** 40 | * 新建一个实例。 41 | * 42 | * @param out 43 | * 输出流 44 | * @param in 45 | * 输入流 46 | * @throws IOException 47 | * @throws InterruptedException 48 | */ 49 | public CliView(PrintStream out, InputStream in) 50 | throws IOException, InterruptedException { 51 | terminal = TerminalBuilder.builder().system(true).streams(in, out).build(); 52 | this.in = in; 53 | this.out = out; 54 | terminal.echo(false); 55 | terminal.enterRawMode(); 56 | logger.info("terminal width: " + terminal.getWidth()); 57 | startCharReading(); 58 | } 59 | 60 | /** 61 | * 初始化显示。 62 | * 63 | * @throws IOException 64 | */ 65 | public synchronized void init() throws IOException { 66 | for (int i = 0; i < terminal.getHeight(); i++) 67 | out.println(); 68 | updateStatus(""); 69 | } 70 | 71 | /** 72 | * 显示信息。 73 | * 74 | * @param message 75 | * 信息 76 | * @throws IOException 77 | */ 78 | public synchronized void printMessage(String message) throws IOException { 79 | String status = this.status; 80 | updateStatus(message); // TODO message过长会导致显示窗口太窄 81 | out.println(); 82 | out.print(status); 83 | this.status = status; 84 | } 85 | 86 | /** 87 | * 显示分割线。 88 | * 89 | * @param message 90 | * 分割线中间显示的信息,null表示没有信息 91 | * @param width 92 | * 宽度 93 | * @throws IOException 94 | */ 95 | public synchronized void printSplitLine(String message, int width) 96 | throws IOException { 97 | int messageLength = 0; 98 | if (message != null) 99 | messageLength = strWidth(message); 100 | 101 | StringBuilder line = new StringBuilder(); 102 | if (messageLength == 0) 103 | for (int i = 0; i < width; i++) 104 | line.append(SPLIT_LINE_CHAR); 105 | else if (messageLength >= width - 2) 106 | line.append(message); 107 | else { 108 | double halfLineLength = (width - messageLength - 2) / 2d; 109 | for (int i = 0; i < Math.floor(halfLineLength); i++) 110 | line.append(SPLIT_LINE_CHAR); 111 | line.append(' '); 112 | line.append(message); 113 | line.append(' '); 114 | for (int i = 0; i < Math.ceil(halfLineLength); i++) 115 | line.append(SPLIT_LINE_CHAR); 116 | } 117 | printMessage(line.toString()); 118 | } 119 | 120 | /** 121 | * 更新状态栏信息。 122 | * 123 | * @param status 124 | * 状态栏信息 125 | */ 126 | public synchronized void updateStatus(String status) { 127 | int strWidth = strWidth(status); 128 | int terminalWidth = terminal.getWidth(); 129 | if (strWidth > terminalWidth) 130 | status = WINDOW_TOO_NARROW.str();// TODO 窗口太窄 131 | 132 | int narrowed = strWidth(this.status) - strWidth(status); 133 | out.print('\r'); 134 | this.status = status; 135 | out.print(status); 136 | if (narrowed > 0) { 137 | for (int i = 0; i < narrowed; i++) 138 | out.print(' '); 139 | for (int i = 0; i < narrowed; i++) 140 | out.print('\b'); 141 | } 142 | } 143 | 144 | /** 145 | * 添加一个字符处理器。添加后,读取的所有字符都将交给此字符处理器处理。 146 | * 147 | * @param handler 148 | * 字符处理器 149 | * @param wait 150 | * 是否等待,直到此监听器停止监听(被移除),此方法才返回 151 | * @throws InterruptedException 152 | */ 153 | public void addCharHandler(CharHandler handler, boolean wait) 154 | throws InterruptedException { 155 | charHandlers.add(handler); 156 | if (wait) { 157 | synchronized (handler) { 158 | try { 159 | while (charHandlers.contains(handler)) 160 | handler.wait(); 161 | } finally { 162 | charHandlers.remove(handler); 163 | } 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * 字符处理器。 170 | * 171 | * @author blovemaple 172 | */ 173 | @FunctionalInterface 174 | public static interface CharHandler { 175 | /** 176 | * 处理结果。由处理方法返回。 177 | */ 178 | enum HandlingResult { 179 | /** 180 | * 未处理。 181 | */ 182 | IGNORE, 183 | /** 184 | * 已处理。如果此监听器处于独占状态,将解除独占状态。 185 | */ 186 | ACCEPT, 187 | /** 188 | * 已处理,并且将独占下一个字符的处理。返回此结果后,下一个读入的字符将仅交给此处理器处理。 189 | */ 190 | MONOPOLIZE, 191 | /** 192 | * 已处理,并且不再处理以后的字符。返回此结果后,此处理器将会被移除。 193 | */ 194 | QUIT, 195 | } 196 | 197 | /** 198 | * 处理一个字符。 199 | * 200 | * @param c 201 | * 字符 202 | * @return 处理结果 203 | */ 204 | HandlingResult handle(char c); 205 | } 206 | 207 | private static ReadThread thread; 208 | 209 | private void startCharReading() throws InterruptedException { 210 | synchronized (ReadThread.class) { 211 | if (thread == null || thread.getState() == State.TERMINATED) { 212 | thread = new ReadThread(); 213 | thread.start(); 214 | } 215 | } 216 | } 217 | 218 | @SuppressWarnings("unused") 219 | private void stopCharReading() throws InterruptedException { 220 | synchronized (ReadThread.class) { 221 | if (thread != null && thread.getState() != State.TERMINATED) { 222 | thread.interrupt(); 223 | thread.join(); 224 | } 225 | } 226 | } 227 | 228 | private class ReadThread extends Thread { 229 | 230 | private ReadThread() { 231 | this.setDaemon(true); 232 | } 233 | 234 | @Override 235 | public void run() { 236 | try { 237 | char c; 238 | 239 | while ((c = (char) in.read()) >= 0) { 240 | if (monoHandler != null) { 241 | CharHandler handler = monoHandler; 242 | monoHandler = null; 243 | handle(handler, c); 244 | } else { 245 | for (CharHandler handler : charHandlers) 246 | handle(handler, c); 247 | } 248 | } 249 | } catch (Exception e) { 250 | try { 251 | logger.log(Level.SEVERE, e.toString(), e); 252 | printMessage("[ERROR]" + e.toString()); 253 | } catch (IOException e1) { 254 | logger.log(Level.SEVERE, e.toString(), e); 255 | } 256 | } 257 | } 258 | 259 | private void handle(CharHandler handler, char c) { 260 | switch (handler.handle(c)) { 261 | case MONOPOLIZE: 262 | monoHandler = handler; 263 | break; 264 | case QUIT: 265 | charHandlers.remove(handler); 266 | break; 267 | default: 268 | break; 269 | } 270 | synchronized (handler) { 271 | handler.notifyAll(); 272 | } 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/action/AbstractPlayerActionType.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.action; 2 | 3 | import static com.github.blovemaple.mj.utils.MyUtils.*; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.Objects; 8 | import java.util.Set; 9 | import java.util.function.BiPredicate; 10 | import java.util.function.Predicate; 11 | import java.util.logging.Logger; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | 15 | import com.github.blovemaple.mj.game.GameContext; 16 | import com.github.blovemaple.mj.game.GameContextPlayerView; 17 | import com.github.blovemaple.mj.object.PlayerInfo; 18 | import com.github.blovemaple.mj.object.PlayerLocation; 19 | import com.github.blovemaple.mj.object.Tile; 20 | 21 | /** 22 | * 玩家做出的各种动作类型的共同逻辑。 23 | * 24 | * @author blovemaple 25 | */ 26 | public abstract class AbstractPlayerActionType implements PlayerActionType { 27 | @SuppressWarnings("unused") 28 | private static final Logger logger = Logger 29 | .getLogger(AbstractPlayerActionType.class.getSimpleName()); 30 | 31 | /** 32 | * 先使用{@link #meetPrecondition}检查前提条件,如果满足再调用{@link #canDoWithPrecondition} 33 | * 。 34 | * 35 | * @see com.github.blovemaple.mj.action.ActionType#canDo(com.github.blovemaple.mj.game.GameContext, 36 | * com.github.blovemaple.mj.object.PlayerLocation) 37 | */ 38 | @Override 39 | public boolean canDo(GameContext context, PlayerLocation location) { 40 | if (!meetPrecondition(context.getPlayerView(location))) 41 | return false; 42 | return canDoWithPrecondition(context, location); 43 | } 44 | 45 | /** 46 | * 判断当前状态下指定玩家是否符合做出此类型动作的前提条件(比如“碰”的前提条件是别人刚出牌)。
    47 | * 默认实现调用相应方法对上一个动作和活牌数量进行限制,进行判断。 48 | */ 49 | protected boolean meetPrecondition(GameContextPlayerView context) { 50 | // 验证听牌条件 51 | if (!isAllowedInTing() && context.getMyInfo().isTing()) 52 | return false; 53 | 54 | // 验证aliveTiles数量条件 55 | Predicate aliveTileSizeCondition = getAliveTileSizePrecondition(); 56 | if (aliveTileSizeCondition != null) 57 | if (!aliveTileSizeCondition 58 | .test(context.getMyInfo().getAliveTiles().size())) 59 | return false; 60 | 61 | // 验证上一个动作条件 62 | BiPredicate lastActionPrecondition = getLastActionPrecondition(); 63 | if (lastActionPrecondition != null) { 64 | Action lastAction = context.getLastAction(); 65 | if (lastAction != null) 66 | if (!lastActionPrecondition.test(lastAction, 67 | context.getMyLocation())) 68 | return false; 69 | } 70 | 71 | return true; 72 | } 73 | 74 | /** 75 | * 返回听牌时是否可进行此动作。默认true。 76 | */ 77 | protected boolean isAllowedInTing() { 78 | return true; 79 | } 80 | 81 | /** 82 | * 返回进行此类型动作时对上一个动作和本家位置的限制条件。
    83 | * 不允许返回null,不限制应该返回恒null的函数。默认返回恒true。
    84 | * 此方法用于{@link #meetPrecondition}的默认实现。 85 | */ 86 | protected BiPredicate getLastActionPrecondition() { 87 | return (a, l) -> true; 88 | } 89 | 90 | /** 91 | * 返回进行此类型动作时对对活牌数量的限制条件。
    92 | * 不允许返回null,不限制应该返回恒null的函数。默认返回恒null。
    93 | * 此方法用于{@link #meetPrecondition}的默认实现。 94 | */ 95 | protected Predicate getAliveTileSizePrecondition() { 96 | return s -> true; 97 | } 98 | 99 | /** 100 | * 判断指定状态下指定位置的玩家可否做此种类型的动作。调用此方法之前已经使用{@link #meetPrecondition}判断过符合前提条件。 101 | *
    102 | * 默认实现为:判断{@link #legalActionTilesStream}返回的流不为空。 103 | */ 104 | protected boolean canDoWithPrecondition(GameContext context, 105 | PlayerLocation location) { 106 | return legalActionTilesStream(context.getPlayerView(location)).findAny() 107 | .isPresent(); 108 | } 109 | 110 | @Override 111 | public Collection> getLegalActionTiles(GameContextPlayerView context) { 112 | if (!meetPrecondition(context)) 113 | return Collections.emptySet(); 114 | return legalActionTilesStream(context).collect(Collectors.toSet()); 115 | } 116 | 117 | /** 118 | * {@inheritDoc}
    119 | * 默认实现为将action为null的和动作类型不符合的报异常,然后用{@link #isLegalActionTiles}检查是否合法。 120 | * 121 | * @see com.github.blovemaple.mj.action.ActionType#isLegalAction(GameContext, 122 | * com.github.blovemaple.mj.action.Action) 123 | */ 124 | @Override 125 | public boolean isLegalAction(GameContext context, Action action) { 126 | Objects.requireNonNull(action); 127 | if (!(action instanceof PlayerAction)) 128 | throw new IllegalArgumentException(action + " is not a PlayerAction"); 129 | if (!matchBy(action.getType())) 130 | throw new IllegalArgumentException( 131 | action.getType().getRealTypeClass().getSimpleName() + " is not " + getRealTypeClass()); 132 | if (!isLegalActionTiles(context.getPlayerView(((PlayerAction) action).getLocation()), 133 | ((PlayerAction) action).getTiles())) 134 | return false; 135 | return true; 136 | } 137 | 138 | /** 139 | * {@inheritDoc}
    140 | * 默认实现为用{@link #isLegalAction}检查是否合法,如果合法则调用{@link #doLegalAction}执行动作。 141 | * 142 | * @see com.github.blovemaple.mj.action.ActionType#doAction(GameContext, 143 | * com.github.blovemaple.mj.action.Action) 144 | */ 145 | @Override 146 | public void doAction(GameContext context, Action action) throws IllegalActionException { 147 | if (!isLegalAction(context, action)) 148 | throw new IllegalActionException(context, action); 149 | 150 | doLegalAction(context, ((PlayerAction)action).getLocation(), ((PlayerAction)action).getTiles()); 151 | } 152 | 153 | /** 154 | * 返回一个流,流中包含指定状态下指定玩家可作出的此类型的所有合法动作的相关牌集合。
    155 | * 默认实现为:在玩家手中的牌中选取所有合法数量个牌的组合,并使用{@link #isLegalActionTiles}过滤出合法的组合。
    156 | * 如果合法的相关牌不限于手中的牌,则需要子类重写此方法。 157 | */ 158 | protected Stream> legalActionTilesStream(GameContextPlayerView context) { 159 | PlayerInfo playerInfo = context.getMyInfo(); 160 | if (playerInfo == null) 161 | return Stream.empty(); 162 | return combSetStream( 163 | getActionTilesRange(context, context.getMyLocation()), 164 | getActionTilesSize()) 165 | .filter(tiles -> isLegalActionTiles(context, tiles)); 166 | } 167 | 168 | /** 169 | * 返回合法动作中相关牌的可选范围。
    170 | * 默认实现为指定玩家的aliveTiles。 171 | */ 172 | protected Set getActionTilesRange(GameContextPlayerView context, PlayerLocation location) { 173 | return context.getMyInfo().getAliveTiles(); 174 | } 175 | 176 | /** 177 | * 返回合法动作中相关牌的数量。可以为0。 178 | */ 179 | protected abstract int getActionTilesSize(); 180 | 181 | /** 182 | * 判断动作是否合法。
    183 | * 默认实现为:先检查前提条件、相关牌数量、相关牌范围,如果满足再调用{@link #isLegalActionWithPreconition}。 184 | */ 185 | protected boolean isLegalActionTiles(GameContextPlayerView context, Set tiles) { 186 | PlayerLocation location = context.getMyLocation(); 187 | if (!meetPrecondition(context)) { 188 | return false; 189 | } 190 | 191 | int legalTilesSize = getActionTilesSize(); 192 | if (legalTilesSize > 0 193 | && (tiles == null || tiles.size() != legalTilesSize)) { 194 | return false; 195 | } 196 | 197 | Set legalTilesRange = getActionTilesRange(context, location); 198 | if (tiles != null && legalTilesRange != null 199 | && !legalTilesRange.containsAll(tiles)) { 200 | return false; 201 | } 202 | 203 | boolean legal = isLegalActionWithPreconition(context, tiles); 204 | return legal; 205 | } 206 | 207 | /** 208 | * 判断动作是否合法。调用此方法之前已经判断确保符合前提条件、相关牌数量、相关牌范围。 209 | */ 210 | protected abstract boolean isLegalActionWithPreconition(GameContextPlayerView context, 211 | Set tiles); 212 | 213 | /** 214 | * 执行动作。调用此方法之前已经确保符合动作类型,并使用{@link #isLegalActionTiles}判断过动作的合法性。 215 | */ 216 | protected abstract void doLegalAction(GameContext context, 217 | PlayerLocation location, Set tiles); 218 | 219 | @Override 220 | public String toString() { 221 | return name(); 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /src/test/java/com/github/blovemaple/mj/local/foobot/NormalWinTypeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.foobot; 2 | 3 | import static com.github.blovemaple.mj.object.TileRank.NumberRank.*; 4 | import static com.github.blovemaple.mj.object.TileSuit.*; 5 | 6 | import java.util.Collection; 7 | 8 | import org.junit.After; 9 | import org.junit.AfterClass; 10 | import org.junit.Before; 11 | import org.junit.BeforeClass; 12 | import org.junit.Test; 13 | 14 | import com.github.blovemaple.mj.object.PlayerInfo; 15 | import com.github.blovemaple.mj.object.Tile; 16 | import com.github.blovemaple.mj.object.TileType; 17 | import com.github.blovemaple.mj.rule.simple.NormalWinType; 18 | import com.github.blovemaple.mj.rule.win.WinInfo; 19 | 20 | public class NormalWinTypeTest { 21 | private NormalWinType winType = NormalWinType.get(); 22 | private PlayerInfo selfInfo; 23 | private Collection candidates; 24 | 25 | @BeforeClass 26 | public static void setUpBeforeClass() throws Exception { 27 | } 28 | 29 | @AfterClass 30 | public static void tearDownAfterClass() throws Exception { 31 | } 32 | 33 | @Before 34 | public void setUp() throws Exception { 35 | candidates = Tile.all(); 36 | 37 | selfInfo = new PlayerInfo(); 38 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SAN), 0)); 39 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SAN), 1)); 40 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, QI), 1)); 41 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, LIU), 1)); 42 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, SI), 0)); 43 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, LIU), 0)); 44 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, QI), 0)); 45 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 1)); 46 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, LIU), 1)); 47 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, QI), 1)); 48 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 2)); 49 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, LIU), 2)); 50 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, QI), 2)); 51 | 52 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, YI), 0)); 53 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SI), 1)); 54 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, QI), 1)); 55 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, YI), 1)); 56 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, SI), 1)); 57 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, QI), 1)); 58 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, YI), 1)); 59 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 1)); 60 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, QI), 1)); 61 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, DONG_FENG), 2)); 62 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, NAN), 2)); 63 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, BEI), 2)); 64 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, XI), 1)); 65 | 66 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, WU), 0)); 67 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, LIU), 1)); 68 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, QI), 1)); 69 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, BA), 1)); 70 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, NAN), 2)); 71 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, XI), 1)); 72 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, BEI), 2)); 73 | // selfInfo.setLastDrawedTile(Tile.of(TileType.of(WAN, LIU), 1)); 74 | 75 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, YI), 0)); 76 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SI), 1)); 77 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, BA), 1)); 78 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, ER), 1)); 79 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, JIU), 1)); 80 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 0)); 81 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 1)); 82 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, WU), 0)); 83 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, JIU), 0)); 84 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, NAN), 1)); 85 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, NAN), 2)); 86 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, XI), 1)); 87 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, BEI), 2)); 88 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(ZI, BAI), 3)); 89 | // selfInfo.setLastDrawedTile(Tile.of(TileType.of(BING, SI), 1)); 90 | 91 | // WIN 92 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 1)); 93 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 0)); 94 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 2)); 95 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, LIU), 1)); 96 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, LIU), 0)); 97 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, LIU), 2)); 98 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, WU), 1)); 99 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, WU), 0)); 100 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, WU), 2)); 101 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SAN), 0)); 102 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SAN), 1)); 103 | 104 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SAN), 0)); 105 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SAN), 1)); 106 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, JIU), 0)); 107 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, JIU), 1)); 108 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, JIU), 2)); 109 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, QI), 1)); 110 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, BA), 1)); 111 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, JIU), 1)); 112 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, YI), 1)); 113 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, ER), 1)); 114 | // selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SAN), 1)); 115 | 116 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, YI), 0)); 117 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, ER), 1)); 118 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(WAN, SAN), 2)); 119 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, SAN), 2)); 120 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, SI), 2)); 121 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, WU), 2)); 122 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, JIU), 2)); 123 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(TIAO, JIU), 3)); 124 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SAN), 1)); 125 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, SI), 1)); 126 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, WU), 1)); 127 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, QI), 1)); 128 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, BA), 1)); 129 | selfInfo.getAliveTiles().add(Tile.of(TileType.of(BING, JIU), 1)); 130 | } 131 | 132 | @After 133 | public void tearDown() throws Exception { 134 | } 135 | 136 | // @Test 137 | public void test() { 138 | // winType.changingsForWin(selfInfo, 0, 139 | // candidates).forEach(System.out::println); 140 | winType.changingsForWin(selfInfo, 0, candidates).count(); 141 | } 142 | 143 | @Test 144 | public void testWin() { 145 | WinInfo winInfo = WinInfo.fromPlayerTiles(selfInfo, null, false); 146 | System.out.println(winType.match(winInfo)); 147 | } 148 | 149 | @Test 150 | public void testGetDiscard() { 151 | winType.getDiscardCandidates(selfInfo.getAliveTiles(), candidates).forEach(System.out::println); 152 | } 153 | 154 | @Test 155 | public void testParse() { 156 | WinInfo winInfo = WinInfo.fromPlayerTiles(selfInfo, null, false); 157 | winType.parseWinTileUnits(winInfo).forEach(unitList -> { 158 | unitList.forEach(System.out::println); 159 | System.out.println(); 160 | }); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/github/blovemaple/mj/local/bazbot/BazBotTileNeighborhood.java: -------------------------------------------------------------------------------- 1 | package com.github.blovemaple.mj.local.bazbot; 2 | 3 | import static com.github.blovemaple.mj.object.StandardTileUnitType.*; 4 | import static com.github.blovemaple.mj.utils.LambdaUtils.*; 5 | import static com.github.blovemaple.mj.utils.MyUtils.*; 6 | import static java.util.Comparator.*; 7 | import static java.util.stream.Collectors.*; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Set; 12 | import java.util.concurrent.ExecutionException; 13 | import java.util.logging.Logger; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.IntStream; 16 | 17 | import org.apache.commons.lang3.builder.ToStringBuilder; 18 | import org.apache.commons.lang3.builder.ToStringStyle; 19 | 20 | import com.github.blovemaple.mj.local.bazbot.BazBotTileUnit.BazBotTileUnitType; 21 | import com.github.blovemaple.mj.object.Tile; 22 | import com.github.blovemaple.mj.object.TileRank.NumberRank; 23 | import com.google.common.cache.Cache; 24 | import com.google.common.cache.CacheBuilder; 25 | 26 | /** 27 | * 两两可组成{@link BazBotTileUnit}(每张牌至少能和另一张牌组成一个{@link BazBotTileUnit})的一组牌。 28 | * 29 | * @author blovemaple 30 | */ 31 | class BazBotTileNeighborhood { 32 | private static final Logger logger = Logger.getLogger(BazBotTileNeighborhood.class.getSimpleName()); 33 | 34 | private static final Cache, BazBotTileNeighborhood> cache = // 35 | CacheBuilder.newBuilder().maximumSize(200).build(); 36 | 37 | /** 38 | * 把指定的Tile集合解析为若干个neiborhood并返回。 39 | */ 40 | public static List parse(Set tiles) { 41 | List tileList = tiles.stream() // 42 | .sorted(comparing(Tile::type).thenComparing(Tile::id)) // 43 | .collect(Collectors.toList()); 44 | 45 | List> neighborhoodTilesList = new ArrayList<>(); 46 | List crtNeighbors = null; 47 | Tile lastTile = null; 48 | for (Tile tile : tileList) { 49 | if (crtNeighbors == null || !isNeighbors(lastTile, tile)) { 50 | crtNeighbors = new ArrayList<>(); 51 | neighborhoodTilesList.add(crtNeighbors); 52 | } 53 | crtNeighbors.add(tile); 54 | lastTile = tile; 55 | } 56 | try { 57 | return neighborhoodTilesList.stream().map(rethrowFunction(hoodTiles -> { 58 | if (cache.getIfPresent(hoodTiles) != null) 59 | logger.fine(() -> "BazBotTileNeighborhood Cache hit."); 60 | else 61 | logger.fine(() -> "BazBotTileNeighborhood Cache NO hit."); 62 | return cache.get(hoodTiles, () -> new BazBotTileNeighborhood(hoodTiles)); 63 | })).collect(Collectors.toList()); 64 | } catch (ExecutionException e) { 65 | // not possible 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | 70 | private static boolean isNeighbors(Tile tile1, Tile tile2) { 71 | if (tile1.type() == tile2.type()) 72 | return true; 73 | 74 | if (tile1.type().suit() != tile2.type().suit()) 75 | return false; 76 | if (tile1.type().suit().getRankClass() != NumberRank.class) 77 | return false; 78 | int number1 = ((NumberRank) tile1.type().rank()).number(); 79 | int number2 = ((NumberRank) tile2.type().rank()).number(); 80 | if (number2 - number1 > 2) 81 | return false; 82 | return true; 83 | } 84 | 85 | private List tiles; 86 | 87 | // 完整将牌、完整顺刻、不完整将牌、不完整顺刻(缺一张)、不完整顺刻(缺两张) 88 | private boolean parsed = false; 89 | private List completedJiangs = new ArrayList<>(); 90 | private List completedShunKes = new ArrayList<>(); 91 | private List uncompletedJiangs = new ArrayList<>(); 92 | private List uncompletedShunKesForOne = new ArrayList<>(); 93 | private List uncompletedShunKesForTwo = new ArrayList<>(); 94 | private transient List allUnits = new ArrayList<>(); 95 | 96 | /** 97 | * @param tiles 98 | * 注意:传入后不会做校验,调用者必须保证tiles是排好序的,并且是一个neighborhood中的牌。 99 | */ 100 | private BazBotTileNeighborhood(List tiles) { 101 | this.tiles = tiles; 102 | } 103 | 104 | private void initParseUnits() { 105 | if (parsed) 106 | return; 107 | 108 | synchronized (this) { 109 | if (parsed) 110 | return; 111 | parseUnits(); 112 | allUnits.addAll(completedJiangs); 113 | allUnits.addAll(completedShunKes); 114 | allUnits.addAll(uncompletedJiangs); 115 | allUnits.addAll(uncompletedShunKesForOne); 116 | allUnits.addAll(uncompletedShunKesForTwo); 117 | parsed = true; 118 | } 119 | } 120 | 121 | private void parseUnits() { 122 | parseUnits(true, this.tiles); 123 | } 124 | 125 | private void parseUnits(boolean init, List tiles) { 126 | if (init && tiles.size() == 3) { 127 | if (SHUNZI.isLegalTiles(tiles)) { 128 | // 整个neighborhood正好是一个顺子,不再拆开 129 | completedShunKes 130 | .add(BazBotTileUnit.completed(SHUNZI, Set.of(tiles.get(0), tiles.get(1), tiles.get(2)), this)); 131 | return; 132 | } 133 | if (KEZI.isLegalTiles(tiles)) { 134 | // 整个neighborhood正好是一个刻子,不再拆开 135 | completedShunKes 136 | .add(BazBotTileUnit.completed(KEZI, Set.of(tiles.get(0), tiles.get(1), tiles.get(2)), this)); 137 | return; 138 | } 139 | } 140 | 141 | // 第一张牌自己是不完整的将牌、顺子、刻子 142 | Tile tile0 = tiles.get(0); 143 | uncompletedJiangs.add(BazBotTileUnit.uncompleted(JIANG, Set.of(tile0), this)); 144 | uncompletedShunKesForTwo.add(BazBotTileUnit.uncompleted(SHUNZI, Set.of(tile0), this)); 145 | uncompletedShunKesForTwo.add(BazBotTileUnit.uncompleted(KEZI, Set.of(tile0), this)); 146 | 147 | // 从第二张牌起,与每张neighbor去重后组成完整将牌、不完整顺子/刻子 148 | if (tiles.size() >= 2) { 149 | tiles.subList(1, tiles.size()).stream() // 150 | .filter(distinctorBy(Tile::type)) // 151 | .takeWhile(tileN -> isNeighbors(tile0, tileN)) // 152 | .forEach(tileN -> { 153 | if (tile0.type() == tileN.type()) { 154 | completedJiangs.add(BazBotTileUnit.completed(JIANG, Set.of(tile0, tileN), this)); 155 | uncompletedShunKesForOne.add(BazBotTileUnit.uncompleted(KEZI, Set.of(tile0, tileN), this)); 156 | } else { 157 | uncompletedShunKesForOne 158 | .add(BazBotTileUnit.uncompleted(SHUNZI, Set.of(tile0, tileN), this)); 159 | } 160 | }); 161 | } 162 | 163 | // 从第二张牌起,与每两张neighbors组成完整顺子/刻子 164 | if (tiles.size() >= 3) { 165 | if (tile0.type() == tiles.get(1).type() && tile0.type() == tiles.get(2).type()) 166 | // 牌型相同的三张牌,是完整的刻子 167 | completedShunKes.add(BazBotTileUnit.completed(KEZI, Set.of(tile0, tiles.get(1), tiles.get(2)), this)); 168 | if (tile0.type().suit().getRankClass() == NumberRank.class && tile0.type().number() <= 7) { 169 | int rank0 = tile0.type().number(); 170 | Tile tile1 = null, tile2 = null; 171 | for (int i = 1; i < tiles.size(); i++) { 172 | Tile tileI = tiles.get(i); 173 | if (tile1 == null && tileI.type().number() == rank0 + 1) { 174 | tile1 = tileI; 175 | } else if (tile2 == null && tileI.type().number() == rank0 + 2) { 176 | tile2 = tileI; 177 | break; 178 | } 179 | } 180 | if (tile1 != null && tile2 != null) 181 | // 数字连续的三张牌,是完整的顺子 182 | completedShunKes.add(BazBotTileUnit.completed(SHUNZI, Set.of(tile0, tile1, tile2), this)); 183 | } 184 | } 185 | 186 | // 递归parse从与第一张不同牌型的牌开始的子列表 187 | if (tiles.size() > 1) 188 | IntStream.range(1, tiles.size()) // 189 | .dropWhile(i -> tile0.type() == tiles.get(i).type()) // 190 | .findFirst().ifPresent( // 191 | firstIndexOfDiffType -> parseUnits(false, 192 | tiles.subList(firstIndexOfDiffType, tiles.size()))); 193 | } 194 | 195 | public List getNonConflictingUnits(BazBotTileUnitType type, List conflictings) { 196 | initParseUnits(); 197 | 198 | List allUnits; 199 | switch (type) { 200 | case COMPLETE_JIANG: 201 | allUnits = completedJiangs; 202 | break; 203 | case COMPLETE_SHUNKE: 204 | allUnits = completedShunKes; 205 | break; 206 | case UNCOMPLETE_JIANG: 207 | allUnits = uncompletedJiangs; 208 | break; 209 | case UNCOMPLETE_SHUNKE_FOR_ONE: 210 | allUnits = uncompletedShunKesForOne; 211 | break; 212 | case UNCOMPLETE_SHUNKE_FOR_TWO: 213 | allUnits = uncompletedShunKesForTwo; 214 | break; 215 | default: 216 | throw new RuntimeException("Unrecignized BazBotTileUnitType: " + type); 217 | } 218 | if (allUnits.isEmpty()) 219 | return List.of(); 220 | 221 | Set conflictTiles = conflictings.stream().map(BazBotTileUnit::tiles).flatMap(Set::stream) 222 | .collect(toSet()); 223 | return allUnits.stream().filter(unit -> !unit.conflictWith(conflictTiles)).collect(toList()); 224 | } 225 | 226 | public List getRemainingTiles(List chosenUnits) { 227 | if (chosenUnits.isEmpty()) 228 | return new ArrayList<>(tiles); 229 | List remainingTiles = new ArrayList<>(tiles); 230 | chosenUnits.stream().map(BazBotTileUnit::tiles).forEach(remainingTiles::removeAll); 231 | return remainingTiles; 232 | } 233 | 234 | @Override 235 | public String toString() { 236 | initParseUnits(); 237 | return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); 238 | } 239 | } 240 | --------------------------------------------------------------------------------