:ruby
    def evaluate(board)
      # Check rows
      (0..8).step(3) do |i|
        if board[i] == board[i + 1] && board[i] == board[i + 2]
          return -10 if board[i] == 'O'
          return 10 if board[i] == 'X'
        end
      end

      # Check columns
      (0..2).each do |i|
        if board[i] == board[i + 3] && board[i] == board[i + 6]
          return -10 if board[i] == 'O'
          return 10 if board[i] == 'X'
        end
      end

      # Check diagonals
      if board[0] == board[4] && board[0] == board[8]
        return -10 if board[0] == 'O'
        return 10 if board[0] == 'X'
      end
      if board[2] == board[4] && board[2] == board[6]
        return -10 if board[2] == 'O'
        return 10 if board[2] == 'X'
      end

      # No winner yet
      0
    end

    def is_draw(board)
      board.none? { |cell| cell == '-' }
    end

    def minimax(board, depth, is_maximizing_player, memo = {})
      # Check for terminal states (win/loss/draw)
      score = evaluate(board)
      return score - depth if score == 10
      return score + depth if score == -10
      return 0 if is_draw(board)

      # Check if result is memoized
      memo_key = "#{board.join(',')}:#{depth}:#{is_maximizing_player}"
      return memo[memo_key] if memo.key?(memo_key)

      # Recursively evaluate possible moves
      best_score = is_maximizing_player ? -Float::INFINITY : Float::INFINITY
      player = is_maximizing_player ? 'X' : 'O'
      board.each_with_index do |cell, i|
        if cell == '-'
          board[i] = player
          current_score = minimax(board, depth + 1, !is_maximizing_player, memo)
          best_score = [best_score, current_score].max if is_maximizing_player
          best_score = [best_score, current_score].min unless is_maximizing_player
          board[i] = '-'
        end
      end

      # Memoize result and return
      memo[memo_key] = best_score
      best_score
    end 
    
    def find_best_move(board)    
      best_score = -Float::INFINITY
      best_move = -1
      board.each_with_index do |cell, i|
        if cell == '-'
          board[i] = 'X'
          current_score = minimax(board, 0, false)
          if current_score > best_score
            best_score = current_score
            best_move = i
          end
          board[i] = '-'
        end
      end
      best_move
    end
    
    def jump_link(s, player_move)
      board_string = s[:board].join()
      board_string[player_move] = 'X'
      if !s[:board].all?('-')
        board_string[s[:computer_move]] = 'O'
      end
      "#" + board_string
    end
    
    def is_endgame(s)
      current = s[:board].dup
      current[s[:computer_move]] = 'O'
      return evaluate(current) != 0 || is_draw(current)
    end
  
    
    values = ['O', 'X', '-']
    all = values.repeated_permutation(9)
    
    all_solutions = all  
     .filter { |board| (board.count('O') - board.count('X')).abs <= 1 &&  evaluate(board) <= 0 }
     .sort_by { |board| [board.count('X'), board.count('O')] }.reverse!
     .map do |board| {  :board => board, :computer_move => find_best_move(board) } 
    end

- all_solutions.each do |s,j|
    %div.b{:id => s[:board].join() }
        - s[:board].each_with_index  do |c,i| 
            - if s[:computer_move] == i && !s[:board].all?('-')
                %svg.o.s
                    %circle
            - elsif c == '-' && !is_endgame(s)
                %a.s{ :href => jump_link(s, i) }
                    %div
            - elsif c == '-'
                %div.s
            - elsif c == 'O'
                %svg.o.s
                    %circle.c
            - else
                %div.x
View Compiled
body {
  background: #1f3a93;
}

.b {
  position: absolute;
  display: flex;
  flex-wrap: wrap;
  padding-left: 0;
  
  div, svg {
    display: inline-block;
    flex: 0 0 33.333333%;
  }
   
  circle {
    stroke: #f62459;
    cx: 50px; 
    cy: 50px; 
    r: 30px;
    stroke-dasharray: 200px; 
    stroke-dashoffset: 200px; 
    stroke-linecap: "round"; 
    stroke-width: 10px;
    fill: none;
  }
}

.b, :target ~ #--------- {
  .s {
    display:none;
  }
}

:target, #--------- {
  width: 300px;
  height: 300px;
  left: calc(50vw - 150px);
  top: calc(50vh - 150px);
  background-image: url(https://i.imgur.com/JOnn3v1.gif);
  background-repeat:  no-repeat;
  background-size: 100% auto;
  
  .s {
     z-index: 1;
     display: inline-block;
  }
  
  .x {
     z-index: 1;
       display: inline-block;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='x'%3E%3Cstyle%3E%0A* %7B box-sizing: border-box;%0A%7D svg %7B height: 100px; width: 100px;%0A%7D svg path %7B stroke-dashoffset: 0; stroke: %2336dbd7;%0A%7D%0A%3C/style%3E%3Cpath class='cross' d='M 20 20 L 80 80' fill='none' stroke-width='10' stroke-linecap='round' stroke-dasharray='100' stroke-dashoffset='0' stoke='%2336dbd7'%3E%3C/path%3E%3Cpath class='cross' d='M 80 20 L 20 80' fill='none' stroke-width='10' stroke-linecap='round' stroke-dasharray='100' stroke-dashoffset='0' stoke='%2336dbd7'%3E%3C/path%3E%3C/svg%3E%0A");  
    height: 100px;
    width: 100px;
  }

  circle {
    animation-fill-mode: forwards;
    animation-name: draw;
    animation-duration: 1s;
   
    
    &.c {
       animation-play-state: paused;
       animation-delay: -1s;
    }
  }
}

svg {
  height: 100px;
  width: 100px;
}

.o {
  transform: rotateY(180deg) rotate(-35deg);
}

@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}

.s {
  width: 100px;
  height: 100px;
}
View Compiled
/* no script! */
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.