import java.awt.*; import java.awt.event.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.swing.*; public class OthelloTitanPro extends JFrame { // --- プロ仕様設定 --- private static final int DEPTH_MID = 14; // 中盤: 14手読み private static final int DEPTH_END = 22; // 終盤: 22マス空きから完全解析 private static final int TT_SIZE = 1 << 20; // 置換表サイズ private OthelloPanel boardPanel; private JButton btnReplay; // 棋譜再生ボタン public static void main(String[] args) { SwingUtilities.invokeLater(() -> new OthelloTitanPro()); } public OthelloTitanPro() { setTitle("Othello Titan Pro (Fixed Replay Start)"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); boardPanel = new OthelloPanel(this); add(boardPanel, BorderLayout.CENTER); JPanel controls = new JPanel(); controls.setLayout(new FlowLayout()); String[] modes = {"人(黒) vs AI(白)", "AI(黒) vs 人(白)", "人 vs 人", "AI vs AI"}; JComboBox modeCombo = new JComboBox<>(modes); JButton btnHint = new JButton("最強ヒント"); JButton btnEdit = new JButton("編集モード"); JButton btnTurnSwitch = new JButton("手番: 黒"); JButton btnReset = new JButton("初期化"); btnReplay = new JButton("棋譜再生"); btnTurnSwitch.setEnabled(false); btnReplay.setEnabled(false); controls.add(new JLabel("モード:")); controls.add(modeCombo); controls.add(btnHint); controls.add(btnEdit); controls.add(btnTurnSwitch); controls.add(btnReset); controls.add(btnReplay); add(controls, BorderLayout.SOUTH); // UIアクション定義 modeCombo.addActionListener(e -> boardPanel.setGameMode(modeCombo.getSelectedIndex())); btnHint.addActionListener(e -> boardPanel.showHint()); btnEdit.addActionListener(e -> { boolean isEditing = boardPanel.toggleEditMode(); if (isEditing) { btnEdit.setText("対局再開"); btnHint.setEnabled(false); modeCombo.setEnabled(false); btnTurnSwitch.setEnabled(true); btnReplay.setEnabled(false); } else { btnEdit.setText("編集モード"); btnHint.setEnabled(true); modeCombo.setEnabled(true); btnTurnSwitch.setEnabled(false); } btnTurnSwitch.setText("手番: " + (boardPanel.isBlackTurn ? "黒" : "白")); }); btnTurnSwitch.addActionListener(e -> { boardPanel.toggleTurn(); btnTurnSwitch.setText("手番: " + (boardPanel.isBlackTurn ? "黒" : "白")); }); btnReset.addActionListener(e -> { boardPanel.resetGame(); btnEdit.setText("編集モード"); btnHint.setEnabled(true); modeCombo.setEnabled(true); btnTurnSwitch.setEnabled(false); btnTurnSwitch.setText("手番: 黒"); btnReplay.setEnabled(false); }); btnReplay.addActionListener(e -> { boardPanel.startReplay(); }); pack(); setLocationRelativeTo(null); setVisible(true); } public void onGameOver() { btnReplay.setEnabled(true); } // ========================================== // Bitboard // ========================================== static class Bitboard { long player; long opponent; private static final long MASK_NOT_A = 0xfefefefefefefefeL; private static final long MASK_NOT_H = 0x7f7f7f7f7f7f7f7fL; public Bitboard(long p, long o) { this.player = p; this.opponent = o; } public static Bitboard init() { long b = (1L << 28) | (1L << 35); long w = (1L << 27) | (1L << 36); return new Bitboard(b, w); } public Bitboard setStoneAt(int index, int type, boolean isBlackTurn) { long mask = 1L << index; long newPlayer = player & ~mask; long newOpponent = opponent & ~mask; if (isBlackTurn) { if (type == 1) newPlayer |= mask; else if (type == 2) newOpponent |= mask; } else { if (type == 1) newOpponent |= mask; else if (type == 2) newPlayer |= mask; } return new Bitboard(newPlayer, newOpponent); } public int getStoneColorAt(int index, boolean isBlackTurn) { long mask = 1L << index; if ((player & mask) != 0) return isBlackTurn ? 1 : 2; if ((opponent & mask) != 0) return isBlackTurn ? 2 : 1; return 0; } public long getLegalMoves() { long legal = 0; long blank = ~(player | opponent); long opp = opponent; // 左 long t = opp & (player >>> 1) & MASK_NOT_H; for(int i=0; i<5; i++) t |= opp & (t >>> 1) & MASK_NOT_H; legal |= blank & (t >>> 1) & MASK_NOT_H; // 右 t = opp & (player << 1) & MASK_NOT_A; for(int i=0; i<5; i++) t |= opp & (t << 1) & MASK_NOT_A; legal |= blank & (t << 1) & MASK_NOT_A; // 上 t = opp & (player >>> 8); for(int i=0; i<5; i++) t |= opp & (t >>> 8); legal |= blank & (t >>> 8); // 下 t = opp & (player << 8); for(int i=0; i<5; i++) t |= opp & (t << 8); legal |= blank & (t << 8); // 右上 t = opp & (player >>> 7) & MASK_NOT_A; for(int i=0; i<5; i++) t |= opp & (t >>> 7) & MASK_NOT_A; legal |= blank & (t >>> 7) & MASK_NOT_A; // 左上 t = opp & (player >>> 9) & MASK_NOT_H; for(int i=0; i<5; i++) t |= opp & (t >>> 9) & MASK_NOT_H; legal |= blank & (t >>> 9) & MASK_NOT_H; // 右下 t = opp & (player << 9) & MASK_NOT_A; for(int i=0; i<5; i++) t |= opp & (t << 9) & MASK_NOT_A; legal |= blank & (t << 9) & MASK_NOT_A; // 左下 t = opp & (player << 7) & MASK_NOT_H; for(int i=0; i<5; i++) t |= opp & (t << 7) & MASK_NOT_H; legal |= blank & (t << 7) & MASK_NOT_H; return legal; } public Bitboard makeMove(long moveBit) { long rev = 0; for (int k = 0; k < 8; k++) { long rev_ = 0; long mask = transfer(moveBit, k); while (mask != 0 && (mask & opponent) != 0) { rev_ |= mask; mask = transfer(mask, k); } if ((mask & player) != 0) { rev |= rev_; } } return new Bitboard(opponent ^ rev, player | moveBit | rev); } private long transfer(long put, int dir) { switch(dir) { case 0: return (put >>> 8); case 1: return (put >>> 7) & MASK_NOT_A; case 2: return (put << 1) & MASK_NOT_A; case 3: return (put << 9) & MASK_NOT_A; case 4: return (put << 8); case 5: return (put << 7) & MASK_NOT_H; case 6: return (put >>> 1) & MASK_NOT_H; case 7: return (put >>> 9) & MASK_NOT_H; } return 0; } } // ========================================== // Transposition Table // ========================================== static class TTEntry { long key; int score; int depth; int flag; long bestMove; } static class TranspositionTable { TTEntry[] entries = new TTEntry[TT_SIZE]; static long[][] keys = new long[2][64]; static long turnKey; static { java.util.Random r = new java.util.Random(12345); for(int i=0; i<2; i++) for(int j=0; j<64; j++) keys[i][j] = r.nextLong(); turnKey = r.nextLong(); } public void clear() { Arrays.fill(entries, null); } public long getHash(Bitboard b) { long h = 0; long p = b.player; long o = b.opponent; for(int i=0; i<64; i++) { if (((p >>> i) & 1) == 1) h ^= keys[0][i]; else if (((o >>> i) & 1) == 1) h ^= keys[1][i]; } return h; } public void store(long hash, int score, int depth, int flag, long bestMove) { int idx = (int)(hash & (TT_SIZE - 1)); TTEntry e = entries[idx]; if (e == null || e.depth <= depth) { e = new TTEntry(); e.key = hash; e.score = score; e.depth = depth; e.flag = flag; e.bestMove = bestMove; entries[idx] = e; } } public TTEntry retrieve(long hash) { int idx = (int)(hash & (TT_SIZE - 1)); TTEntry e = entries[idx]; if (e != null && e.key == hash) return e; return null; } } // ========================================== // Pro Solver // ========================================== static class Solver { private static final int[] WEIGHTS = { 120, -20, 20, 5, 5, 20, -20, 120, -20, -40, -5, -5, -5, -5, -40, -20, 20, -5, 15, 3, 3, 15, -5, 20, 5, -5, 3, 3, 3, 3, -5, 5, 5, -5, 3, 3, 3, 3, -5, 5, 20, -5, 15, 3, 3, 15, -5, 20, -20, -40, -5, -5, -5, -5, -40, -20, 120, -20, 20, 5, 5, 20, -20, 120 }; private static TranspositionTable tt = new TranspositionTable(); public static long getBestMove(Bitboard b, int depth) { int empty = 64 - Long.bitCount(b.player | b.opponent); if (empty <= DEPTH_END) { tt.clear(); return solveExact(b, -Integer.MAX_VALUE, Integer.MAX_VALUE); } tt.clear(); long bestMove = 0; for (int d = 1; d <= depth; d++) { bestMove = solveIterative(b, d); } return bestMove; } private static long solveIterative(Bitboard b, int depth) { long legal = b.getLegalMoves(); if (legal == 0) return 0; long bestMove = 0; int maxScore = -Integer.MAX_VALUE; int alpha = -Integer.MAX_VALUE; int beta = Integer.MAX_VALUE; long[] moves = getSortedMoves(legal, b); for (long m : moves) { Bitboard next = b.makeMove(m); int score = -negamax(next, depth - 1, -beta, -alpha); if (score > maxScore) { maxScore = score; bestMove = m; alpha = score; } } return bestMove; } private static long solveExact(Bitboard b, int alpha, int beta) { long legal = b.getLegalMoves(); if (legal == 0) return 0; long bestMove = 0; int maxScore = -Integer.MAX_VALUE; long[] moves = getSortedMoves(legal, b); for (long m : moves) { Bitboard next = b.makeMove(m); int score = -negamax(next, 60, -beta, -alpha); if (score > maxScore) { maxScore = score; bestMove = m; alpha = score; } } return bestMove; } private static int negamax(Bitboard b, int depth, int alpha, int beta) { long hash = tt.getHash(b); TTEntry entry = tt.retrieve(hash); if (entry != null && entry.depth >= depth) { if (entry.flag == 0) return entry.score; if (entry.flag == 1 && entry.score <= alpha) return alpha; if (entry.flag == 2 && entry.score >= beta) return beta; } if (depth <= 0) return evaluate(b); long legal = b.getLegalMoves(); if (legal == 0) { Bitboard passed = new Bitboard(b.opponent, b.player); if (passed.getLegalMoves() == 0) return finalScore(b); return -negamax(passed, depth, -beta, -alpha); } long ttMove = (entry != null) ? entry.bestMove : 0; long[] moves = getSortedMovesWithTT(legal, ttMove); int alphaOrig = alpha; long bestMove = 0; int maxScore = -Integer.MAX_VALUE; for (long m : moves) { Bitboard next = b.makeMove(m); int score = -negamax(next, depth - 1, -beta, -alpha); if (score > maxScore) { maxScore = score; bestMove = m; } if (score > alpha) alpha = score; if (alpha >= beta) break; } int flag = 0; if (maxScore <= alphaOrig) flag = 1; else if (maxScore >= beta) flag = 2; tt.store(hash, maxScore, depth, flag, bestMove); return maxScore; } private static long[] getSortedMoves(long legal, Bitboard b) { return getSortedMovesWithTT(legal, 0); } private static long[] getSortedMovesWithTT(long legal, long ttMove) { int count = Long.bitCount(legal); long[] moves = new long[count]; int[] scores = new int[count]; int idx = 0; for(long temp = legal; temp != 0; temp &= (temp - 1)) { long m = Long.lowestOneBit(temp); moves[idx] = m; if (m == ttMove) scores[idx] = 10000; else scores[idx] = WEIGHTS[Long.numberOfTrailingZeros(m)]; idx++; } for(int i=0; i 0 ? 10000 + diff : -10000 + diff; } } // ========================================== // UI Panel // ========================================== class OthelloPanel extends JPanel implements MouseListener { private OthelloTitanPro parentFrame; Bitboard currentBoard; boolean isBlackTurn = true; // ★ここが修正ポイント: 開始局面のスナップショット Bitboard startBoardSnapshot; boolean startTurnSnapshot = true; boolean gameOver = false; int gameMode = 0; boolean isEditMode = false; long hintBit = 0; List moveHistory = new ArrayList<>(); boolean isReplaying = false; Timer replayTimer; int replayIndex = 0; final int CELL_SIZE = 80; public OthelloPanel(OthelloTitanPro frame) { this.parentFrame = frame; setPreferredSize(new Dimension(640, 680)); addMouseListener(this); resetGame(); } public void resetGame() { currentBoard = Bitboard.init(); isBlackTurn = true; // ★リセット時は標準初期配置をスナップショットにする startBoardSnapshot = currentBoard; startTurnSnapshot = true; gameOver = false; isEditMode = false; hintBit = 0; isReplaying = false; if(replayTimer != null) replayTimer.stop(); moveHistory.clear(); repaint(); checkTurn(); } public void setGameMode(int mode) { this.gameMode = mode; if (!isEditMode && !gameOver && !isReplaying) checkTurn(); } public boolean toggleEditMode() { isEditMode = !isEditMode; hintBit = 0; if (isEditMode) { // 編集モードに入った瞬間、一旦履歴はクリア(矛盾防止) moveHistory.clear(); } else { // ★編集モード終了時(対局再開)、この局面を「リプレイ開始地点」として保存 startBoardSnapshot = currentBoard; startTurnSnapshot = isBlackTurn; // ここから新しい履歴を開始 moveHistory.clear(); gameOver = false; checkTurn(); } repaint(); return isEditMode; } public void toggleTurn() { isBlackTurn = !isBlackTurn; currentBoard = new Bitboard(currentBoard.opponent, currentBoard.player); // 手番変更時も新しい開始地点とみなす startBoardSnapshot = currentBoard; startTurnSnapshot = isBlackTurn; moveHistory.clear(); repaint(); } public void startReplay() { if (moveHistory.isEmpty()) return; isReplaying = true; // ★修正: 編集終了時のスナップショット局面に戻す currentBoard = startBoardSnapshot; isBlackTurn = startTurnSnapshot; gameOver = false; hintBit = 0; replayIndex = 0; repaint(); replayTimer = new Timer(3000, e -> playNextReplayMove()); replayTimer.setInitialDelay(1500); replayTimer.start(); } private void playNextReplayMove() { if (replayIndex >= moveHistory.size()) { replayTimer.stop(); isReplaying = false; gameOver = true; repaint(); return; } long move = moveHistory.get(replayIndex); replayIndex++; if (move == 0) { // パス currentBoard = new Bitboard(currentBoard.opponent, currentBoard.player); isBlackTurn = !isBlackTurn; } else { // 通常着手 currentBoard = currentBoard.makeMove(move); isBlackTurn = !isBlackTurn; } repaint(); } public void showHint() { if (gameOver || isEditMode || isCurrentPlayerAI() || isReplaying) return; new Thread(() -> { int emptyCount = 64 - Long.bitCount(currentBoard.player | currentBoard.opponent); int depth = (emptyCount <= DEPTH_END) ? 60 : DEPTH_MID; long best = Solver.getBestMove(currentBoard, depth); SwingUtilities.invokeLater(() -> { hintBit = best; repaint(); }); }).start(); } private boolean isCurrentPlayerAI() { if (isEditMode || isReplaying) return false; if (gameMode == 3) return true; if (gameMode == 2) return false; if (gameMode == 0) return !isBlackTurn; if (gameMode == 1) return isBlackTurn; return false; } private void checkTurn() { if (gameOver || isEditMode || isReplaying) return; long legal = currentBoard.getLegalMoves(); if (legal == 0) { Bitboard passed = new Bitboard(currentBoard.opponent, currentBoard.player); if (passed.getLegalMoves() == 0) { gameOver = true; hintBit = 0; repaint(); parentFrame.onGameOver(); return; } moveHistory.add(0L); currentBoard = passed; isBlackTurn = !isBlackTurn; hintBit = 0; repaint(); javax.swing.Timer t = new javax.swing.Timer(500, e -> checkTurn()); t.setRepeats(false); t.start(); return; } if (isCurrentPlayerAI()) { hintBit = 0; repaint(); new Thread(() -> { try { Thread.sleep(200); } catch(Exception e){} int emptyCount = 64 - Long.bitCount(currentBoard.player | currentBoard.opponent); int depth = (emptyCount <= DEPTH_END) ? 60 : DEPTH_MID; long best = Solver.getBestMove(currentBoard, depth); SwingUtilities.invokeLater(() -> executeMove(best)); }).start(); } } private void executeMove(long moveBit) { if (moveBit == 0) return; moveHistory.add(moveBit); currentBoard = currentBoard.makeMove(moveBit); isBlackTurn = !isBlackTurn; hintBit = 0; repaint(); checkTurn(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (int i = 0; i < 64; i++) { int r = i / 8; int c = i % 8; int x = c * CELL_SIZE; int y = r * CELL_SIZE; g2.setColor(new Color(34, 139, 34)); g2.fillRect(x, y, CELL_SIZE, CELL_SIZE); g2.setColor(Color.BLACK); g2.drawRect(x, y, CELL_SIZE, CELL_SIZE); long mask = 1L << i; if ((currentBoard.player & mask) != 0) { g2.setColor(isBlackTurn ? Color.BLACK : Color.WHITE); g2.fillOval(x+8, y+8, CELL_SIZE-16, CELL_SIZE-16); } else if ((currentBoard.opponent & mask) != 0) { g2.setColor(isBlackTurn ? Color.WHITE : Color.BLACK); g2.fillOval(x+8, y+8, CELL_SIZE-16, CELL_SIZE-16); } } if (hintBit != 0) { int hintIdx = Long.numberOfTrailingZeros(hintBit); int r = hintIdx / 8; int c = hintIdx % 8; g2.setColor(Color.MAGENTA); g2.setStroke(new BasicStroke(5)); g2.drawOval(c * CELL_SIZE + 10, r * CELL_SIZE + 10, CELL_SIZE - 20, CELL_SIZE - 20); } if (!isCurrentPlayerAI() && !gameOver && !isEditMode && !isReplaying) { long legal = currentBoard.getLegalMoves(); g2.setColor(new Color(0, 0, 0, 60)); for(int i=0; i<64; i++) { if ((legal & (1L << i)) != 0) { int r = i / 8; int c = i % 8; g2.fillOval(c*CELL_SIZE+35, r*CELL_SIZE+35, 10, 10); } } } g2.setColor(Color.BLACK); g2.fillRect(0, 640, 640, 40); g2.setColor(Color.WHITE); g2.setFont(new Font("SansSerif", Font.BOLD, 16)); String msg = ""; if (isReplaying) { int moves = replayIndex; msg = "【棋譜再生中】 " + moves + "手目 (3秒間隔で再生中...)"; } else if (isEditMode) { msg = "【編集】左:黒 / 右:白 (現在の手番: " + (isBlackTurn ? "黒" : "白") + ")"; } else if (gameOver) { int b = Long.bitCount(isBlackTurn ? currentBoard.player : currentBoard.opponent); int w = Long.bitCount(isBlackTurn ? currentBoard.opponent : currentBoard.player); msg = "終局: 黒 " + b + " - 白 " + w + " (「棋譜再生」で振り返り可能)"; } else { msg = "手番: " + (isBlackTurn ? "黒" : "白") + (isCurrentPlayerAI() ? " (AI長考中)" : " (あなた)"); if (hintBit != 0) msg += " ※最強手を表示"; } g2.drawString(msg, 10, 665); } @Override public void mouseClicked(MouseEvent e) { int c = e.getX() / CELL_SIZE; int r = e.getY() / CELL_SIZE; if (c < 0 || c >= 8 || r < 0 || r >= 8) return; int i = r * 8 + c; if (isReplaying) return; if (isEditMode) { int targetColor = 0; if (SwingUtilities.isLeftMouseButton(e)) targetColor = 1; else if (SwingUtilities.isRightMouseButton(e)) targetColor = 2; if (targetColor == 0) return; int currentColor = currentBoard.getStoneColorAt(i, isBlackTurn); if (currentColor == targetColor) targetColor = 0; currentBoard = currentBoard.setStoneAt(i, targetColor, isBlackTurn); // 編集中の動きは履歴に残さない moveHistory.clear(); repaint(); return; } if (gameOver || isCurrentPlayerAI()) return; long mask = 1L << i; long legal = currentBoard.getLegalMoves(); if ((legal & mask) != 0) { executeMove(mask); } } public void mousePressed(MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} } }