Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ int suggestedDiscardIndex(
if (hand == null || hand.isEmpty()) {
return -1;
}
DiscardChoice best = this.bestDiscardChoice(hand, melds, tingEvaluator);
return best == null ? hand.size() - 1 : best.index();
return this.bestDiscardIndex(hand, melds, tingEvaluator);
}

ReactionResponse suggestedReaction(
Expand Down Expand Up @@ -178,6 +177,54 @@ private long readyScoreForBestDiscard(
return best == null ? 0 : best.readyScore();
}

/**
* Index-only variant of {@link #bestDiscardChoice} for the
* {@link #suggestedDiscardIndex} hot path. Avoids allocating a
* {@link DiscardChoice} record per call when the caller only needs the
* discard index. The loop body is identical; only the return shape
* differs (primitive {@code int} vs record).
*/
private int bestDiscardIndex(
List<MahjongTile> hand,
List<GbMeldState> melds,
TingEvaluator tingEvaluator
) {
int bestIndex = -1;
long bestReadyScore = 0;
int bestDiscardPreference = 0;
GbTingResponse[] tingMemo = new GbTingResponse[TILE_KIND_COUNT];
MahjongTile previousDiscarded = null;
int previousDiscardPreference = 0;
for (int i = 0; i < hand.size(); i++) {
MahjongTile discarded = hand.get(i);
int discardedOrdinal = discarded.ordinal();
GbTingResponse ting = tingMemo[discardedOrdinal];
boolean tingMemoHit = ting != null;
if (ting == null) {
List<MahjongTile> remaining = new ArrayList<>(hand);
remaining.remove(i);
ting = tingEvaluator.evaluate(remaining, melds);
if (ting != null) {
tingMemo[discardedOrdinal] = ting;
}
}
long candidateReadyScore = readyScore(ting);
int candidateDiscardPreference = tingMemoHit && discarded == previousDiscarded
? previousDiscardPreference
: discardPreference(hand, discarded);
previousDiscarded = discarded;
previousDiscardPreference = candidateDiscardPreference;
if (bestIndex < 0
|| candidateReadyScore > bestReadyScore
|| (candidateReadyScore == bestReadyScore && candidateDiscardPreference > bestDiscardPreference)) {
bestIndex = i;
bestReadyScore = candidateReadyScore;
bestDiscardPreference = candidateDiscardPreference;
}
}
return bestIndex < 0 ? hand.size() - 1 : bestIndex;
}

private DiscardChoice bestDiscardChoice(
List<MahjongTile> hand,
List<GbMeldState> melds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,55 @@ static boolean sameKind(MahjongTile left, MahjongTile right) {
return leftBase == rightBase;
}

// Precomputed lookup tables indexed by MahjongTile.ordinal(). Eliminates the
// per-call String allocation (name().substring(1,2)) and Integer.parseInt
// parsing inside hot loops such as GbBotDecisionService.discardPreference.
private static final int[] TILE_NUMBERS = buildTileNumbers();
private static final boolean[] TILE_HONORS = buildTileHonors();

private static int[] buildTileNumbers() {
MahjongTile[] values = MahjongTile.values();
int[] numbers = new int[values.length];
int east = MahjongTile.EAST.ordinal();
int redDragon = MahjongTile.RED_DRAGON.ordinal();
for (MahjongTile tile : values) {
int ord = tile.ordinal();
if (tile.isFlower() || (ord >= east && ord <= redDragon)) {
numbers[ord] = 0;
continue;
}
// UNKNOWN and any other non-suited tile would fail parseInt; leave 0.
// Callers never pass such tiles to tileNumber() (they guard with
// isFlower()/isHonor() or only pass suited tiles from hands/walls).
try {
numbers[ord] = Integer.parseInt(tile.name().substring(1, 2));
} catch (NumberFormatException ignored) {
numbers[ord] = 0;
}
}
return numbers;
}

private static boolean[] buildTileHonors() {
MahjongTile[] values = MahjongTile.values();
boolean[] honors = new boolean[values.length];
int east = MahjongTile.EAST.ordinal();
int redDragon = MahjongTile.RED_DRAGON.ordinal();
for (int i = 0; i < values.length; i++) {
honors[i] = i >= east && i <= redDragon;
}
return honors;
}

static boolean isHonor(MahjongTile tile) {
return tile.ordinal() >= MahjongTile.EAST.ordinal() && tile.ordinal() <= MahjongTile.RED_DRAGON.ordinal();
return TILE_HONORS[tile.ordinal()];
}

static int tileNumber(MahjongTile tile) {
if (tile == null || tile.isFlower() || isHonor(tile)) {
return 0;
}
return Integer.parseInt(tile.name().substring(1, 2));
return TILE_NUMBERS[tile.ordinal()];
}

static MahjongTile offsetTile(MahjongTile tile, int delta) {
Expand Down
Loading