🪶

Cursor Pro Max をGitHub Copilot for xcodeと比較する

に公開

どれほど精度が違うのか?

Cursor EditorにProプランで使用することができるMax Modeなるものがあるらしい?

試すには、$20のプランを契約するだけではダメらしい?
なんと$50~契約しないとダメみたい😇
チャットの画面に表示されるボタンをうっかり押して契約した。。。。。

クレジットカード代が増える笑
気になっていたので試してみよう!

動画も撮ってみました。

https://youtu.be/GjqvbuQdjS8

🎮ゲームでも作るか

なんでもいいから作ってみようと思いました。以前作ったことがあるものを作らせてみる。使用するモデルはこちら💁

Maxのものだけチェックする

やること

  1. Agentモードを選択
  2. gemini-2.5-proかclaude-3.7-sonnetを選択する
  3. どちらでも精度は高かった。何か作らせてみようか

GitHubにサンプルコードございます。

使用するときは、ContentView.swiftでモジュールを読み込んでください。

使用例)

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        VStack {
            CopilotTetrisView()
        }
    }
}

#Preview {
    ContentView()
}

パズルを作ってみた!

色がカラフルなパズルを作ってみました。パズルをタップすると移動します。シュミレーターだと使いづらいかも?

これでパズルが作れます笑

import SwiftUI

struct PuzzleGameView: View {
    // グリッドサイズ: 数字0-9の10個のタイルと1つの空白で3x4グリッド
    private let columns = 3
    private let rows = 4
    private let totalTiles = 12
    
    // タイルの状態を管理
    @State private var tiles: [Tile] = []
    @State private var emptyIndex: Int = 0  // 初期値は後で正しく設定される
    @State private var moveCount = 0
    @State private var isCompleted = false
    
    // ゲーム開始時に実行
    private let colors: [Color] = [
        .red, .orange, .yellow, .green, .blue, .purple,
        .pink, .brown, .mint, .indigo, .cyan
    ]
    
    // 初期化
    init() {
        var initialTiles = createInitialTiles()
        _tiles = State(initialValue: initialTiles.0)
        _emptyIndex = State(initialValue: initialTiles.1)
    }
    
    // 初期タイル作成
    private func createInitialTiles() -> ([Tile], Int) {
        var newTiles: [Tile] = []
        
        // 0から9までの数字タイル作成
        for i in 0...9 {
            newTiles.append(Tile(id: i, value: i, color: colors[i]))
        }
        
        // 空白タイル追加(最後に)
        let emptyTileIndex = 10
        newTiles.append(Tile(id: 10, value: nil, color: .clear))
        
        // 不足しているタイルを追加(3x4=12なので)
        if newTiles.count < totalTiles {
            for i in newTiles.count..<totalTiles {
                newTiles.append(Tile(id: i, value: i, color: .gray))
            }
        }
        
        // タイルをシャッフル
        let (shuffledTiles, newEmptyIndex) = shuffleTiles(newTiles, emptyIndex: emptyTileIndex)
        
        return (shuffledTiles, newEmptyIndex)
    }
    
    // タイルのシャッフル - 空白タイルの新しいインデックスも返す
    private func shuffleTiles(_ tiles: [Tile], emptyIndex: Int) -> ([Tile], Int) {
        var shuffled = tiles
        var currentEmptyIndex = emptyIndex
        
        // ランダムに100回動かす
        for _ in 0..<100 {
            let movableTiles = getMovableTileIndices(emptyIndex: currentEmptyIndex)
            if let randomIndex = movableTiles.randomElement() {
                // タイルを交換
                shuffled.swapAt(randomIndex, currentEmptyIndex)
                // 空白の位置を更新
                currentEmptyIndex = randomIndex
            }
        }
        
        return (shuffled, currentEmptyIndex)
    }
    
    // 移動可能なタイルのインデックスを取得
    private func getMovableTileIndices(emptyIndex: Int) -> [Int] {
        var indices: [Int] = []
        
        // 上のタイル
        if emptyIndex >= columns {
            indices.append(emptyIndex - columns)
        }
        
        // 下のタイル
        if emptyIndex < (rows * columns) - columns {
            indices.append(emptyIndex + columns)
        }
        
        // 左のタイル
        if emptyIndex % columns != 0 {
            indices.append(emptyIndex - 1)
        }
        
        // 右のタイル
        if emptyIndex % columns != columns - 1 {
            indices.append(emptyIndex + 1)
        }
        
        return indices
    }
    
    // タイルをタップした時の処理
    private func moveTile(_ index: Int) {
        // タイル数の範囲外チェック
        guard index >= 0 && index < tiles.count else { return }
        guard emptyIndex >= 0 && emptyIndex < tiles.count else { return }
        
        // 移動可能なタイルのリスト
        let movableTiles = getMovableTileIndices(emptyIndex: emptyIndex)
        
        // 移動可能であれば
        if movableTiles.contains(index) {
            withAnimation(.spring()) {
                // タイルを交換
                tiles.swapAt(index, emptyIndex)
                // 空白の位置を更新
                emptyIndex = index
                // 移動回数を増やす
                moveCount += 1
                // 完成したかチェック
                checkCompletion()
            }
        }
    }
    
    // パズル完成チェック
    private func checkCompletion() {
        // 最初の10個のタイルを確認
        for i in 0..<10 {
            if i >= tiles.count || tiles[i].id != i {
                return
            }
        }
        
        // 空白タイルの位置を確認
        if 10 < tiles.count && tiles[10].value == nil {
            isCompleted = true
        }
    }
    
    // リセット
    private func resetGame() {
        let initialTiles = createInitialTiles()
        tiles = initialTiles.0
        emptyIndex = initialTiles.1
        moveCount = 0
        isCompleted = false
    }
    
    var body: some View {
        VStack {
            Text("スライドパズル")
                .font(.largeTitle)
                .padding()
            
            Text("移動回数: \(moveCount)")
                .font(.headline)
                .padding(.bottom)
            
            // パズルグリッド
            LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: columns), spacing: 8) {
                ForEach(0..<min(tiles.count, totalTiles), id: \.self) { index in
                    TileView(tile: tiles[index])
                        .aspectRatio(1, contentMode: .fit)
                        .onTapGesture {
                            moveTile(index)
                        }
                }
            }
            .padding()
            .background(Color.gray.opacity(0.2))
            .cornerRadius(10)
            .padding()
            
            Button("リセット") {
                resetGame()
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
            
            Spacer()
        }
        .alert("おめでとう!", isPresented: $isCompleted) {
            Button("もう一度") {
                resetGame()
            }
        } message: {
            Text("パズルを\(moveCount)回の移動で完成させました!")
        }
    }
}

// タイルデータモデル
struct Tile: Identifiable {
    let id: Int
    let value: Int?
    let color: Color
}

// タイル表示用View
struct TileView: View {
    let tile: Tile
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(tile.value == nil ? Color.clear : tile.color)
                .shadow(radius: tile.value == nil ? 0 : 2)
            
            if let value = tile.value {
                Text("\(value)")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
            }
        }
    }
}

#Preview {
    PuzzleGameView()
} 

テトリスでも作るか。。。

タップのやり方にコツが入ります。いやーでも完成度は高いですね。

import SwiftUI

// テトリミノの形状と色を定義
struct Tetromino {
    let shape: [[Int]]
    let color: Color
    var position: (row: Int, col: Int) // グリッド上の位置

    // テトリミノの回転(右回転)
    func rotated() -> Tetromino {
        let newShape = shape[0].indices.map { col in
            shape.indices.reversed().map { row in
                shape[row][col]
            }
        }
        return Tetromino(shape: newShape, color: color, position: position)
    }

    // 各テトリミノの定義
    static let I = Tetromino(shape: [[1, 1, 1, 1]], color: .cyan, position: (0, 3))
    static let O = Tetromino(shape: [[1, 1], [1, 1]], color: .yellow, position: (0, 4))
    static let T = Tetromino(shape: [[0, 1, 0], [1, 1, 1]], color: .purple, position: (0, 3))
    static let S = Tetromino(shape: [[0, 1, 1], [1, 1, 0]], color: .green, position: (0, 3))
    static let Z = Tetromino(shape: [[1, 1, 0], [0, 1, 1]], color: .red, position: (0, 3))
    static let J = Tetromino(shape: [[1, 0, 0], [1, 1, 1]], color: .blue, position: (0, 3))
    static let L = Tetromino(shape: [[0, 0, 1], [1, 1, 1]], color: .orange, position: (0, 3))

    static func random() -> Tetromino {
        let tetrominos = [I, O, T, S, Z, J, L]
        // 初期位置をリセットして返す
        var randomTetromino = tetrominos.randomElement()!
        randomTetromino.position = (row: 0, col: Int.random(in: 0...(10 - randomTetromino.shape[0].count))) // 列数はボードサイズとテトリミノの幅に依存
        return randomTetromino
    }
}

struct TetrisGameView: View {
    // ゲームボードのサイズ
    let rows: Int = 20
    let cols: Int = 10

    // ゲームの状態
    @State private var gameBoard: [[Color]]
    @State private var currentTetromino: Tetromino
    @State private var nextTetromino: Tetromino
    @State private var score: Int = 0
    @State private var gameOver: Bool = false

    // ゲームタイマー
    let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()

    init() {
        _gameBoard = State(initialValue: Array(repeating: Array(repeating: .gray.opacity(0.2), count: cols), count: rows))
        let initialTetromino = Tetromino.random()
        _currentTetromino = State(initialValue: initialTetromino)
        _nextTetromino = State(initialValue: Tetromino.random())
    }

    var body: some View {
        VStack(spacing: 0) {
            Text("テトリス")
                .font(.largeTitle)
                .padding()

            HStack(alignment: .top) {
                // ゲームボード
                gameBoardView
                    .border(Color.black, width: 1)

                // サイドパネル(スコア、次のテトリミノなど)
                sidePanelView
                    .padding(.leading)
            }

            // 操作ボタン (今回はジェスチャーで操作することが多いが、デバッグ用に配置も可能)
            // controlButtonsView
        }
        .onReceive(timer) { _ in
            if !gameOver {
                moveTetrominoDown()
            }
        }
        .gesture(
            DragGesture(minimumDistance: 20, coordinateSpace: .local)
                .onEnded { value in
                    handleSwipe(translation: value.translation)
                }
        )
        .alert("ゲームオーバー", isPresented: $gameOver) {
            Button("リスタート") {
                restartGame()
            }
        } message: {
            Text("スコア: \(score)")
        }
    }

    // ゲームボードの描画
    private var gameBoardView: some View {
        VStack(spacing: 1) {
            ForEach(0..<rows, id: \.self) { row in
                HStack(spacing: 1) {
                    ForEach(0..<cols, id: \.self) { col in
                        Rectangle()
                            .fill(getCellColor(row: row, col: col))
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .aspectRatio(1, contentMode: .fit)
                    }
                }
            }
        }
        .background(Color.black.opacity(0.1)) // ボードの背景色
    }

    // サイドパネルの描画
    private var sidePanelView: some View {
        VStack(alignment: .leading, spacing: 20) {
            Text("スコア")
                .font(.headline)
            Text("\(score)")
                .font(.title)
                .fontWeight(.bold)

            Text("次のブロック")
                .font(.headline)
            
            // 次のテトリミノを表示
            VStack(spacing: 1) {
                let shape = nextTetromino.shape // オプショナルでないことを確認
                ForEach(shape.indices, id: \.self) { rowIndex in
                    HStack(spacing: 1) {
                        ForEach(shape[rowIndex].indices, id: \.self) { colIndex in
                            Rectangle()
                                .fill(shape[rowIndex][colIndex] == 1 ? nextTetromino.color : Color.clear)
                                .frame(width: 15, height: 15) // 小さめに表示
                        }
                    }
                }
            }
            .padding(5)
            .border(Color.gray)


            Spacer() // 残りのスペースを埋める
        }
        .frame(width: 120) // サイドパネルの幅
    }
    
    // セルの色を取得 (ゲームボードと現在のテトリミノを考慮)
    private func getCellColor(row: Int, col: Int) -> Color {
        // 現在のテトリミノのセルかどうかを確認
        let shape = currentTetromino.shape
        for r in 0..<shape.count {
            for c in 0..<shape[r].count {
                if shape[r][c] == 1 {
                    let boardRow = currentTetromino.position.row + r
                    let boardCol = currentTetromino.position.col + c
                    if boardRow == row && boardCol == col {
                        return currentTetromino.color
                    }
                }
            }
        }
        // ゲームボードの色
        return gameBoard[row][col]
    }

    // テトリミノを下に移動
    private func moveTetrominoDown() {
        var newPosition = currentTetromino.position
        newPosition.row += 1
        if isValidMove(tetromino: currentTetromino, newPosition: newPosition) {
            currentTetromino.position = newPosition
        } else {
            // 固定処理と新しいテトリミノの生成
            placeTetromino()
            clearLines()
            spawnNewTetromino()
            if !isValidMove(tetromino: currentTetromino, newPosition: currentTetromino.position) {
                gameOver = true
            }
        }
    }

    // テトリミノを左に移動
    private func moveTetrominoLeft() {
        var newPosition = currentTetromino.position
        newPosition.col -= 1
        if isValidMove(tetromino: currentTetromino, newPosition: newPosition) {
            currentTetromino.position = newPosition
        }
    }

    // テトリミノを右に移動
    private func moveTetrominoRight() {
        var newPosition = currentTetromino.position
        newPosition.col += 1
        if isValidMove(tetromino: currentTetromino, newPosition: newPosition) {
            currentTetromino.position = newPosition
        }
    }

    // テトリミノを回転
    private func rotateTetromino() {
        let rotatedTetromino = currentTetromino.rotated()
        if isValidMove(tetromino: rotatedTetromino, newPosition: rotatedTetromino.position) {
            currentTetromino = rotatedTetromino
        }
    }
    
    // スワイプ操作の処理
    private func handleSwipe(translation: CGSize) {
        if gameOver { return }

        let horizontalMovement = translation.width
        let verticalMovement = translation.height

        if abs(horizontalMovement) > abs(verticalMovement) { // 横スワイプ
            if horizontalMovement < 0 { // 左スワイプ
                moveTetrominoLeft()
            } else { // 右スワイプ
                moveTetrominoRight()
            }
        } else { // 縦スワイプ
            if verticalMovement > 0 { // 下スワイプ (ハードドロップまたは高速落下)
                 // 今回はシンプルに一段下げるが、ハードドロップも実装可能
                moveTetrominoDown()
            } else { // 上スワイプ (回転)
                rotateTetromino()
            }
        }
    }

    // 移動が有効かどうかをチェック
    private func isValidMove(tetromino: Tetromino, newPosition: (row: Int, col: Int)) -> Bool {
        let shape = tetromino.shape
        for r in 0..<shape.count {
            for c in 0..<shape[r].count {
                if shape[r][c] == 1 {
                    let boardRow = newPosition.row + r
                    let boardCol = newPosition.col + c

                    // 壁との衝突チェック
                    if boardRow < 0 || boardRow >= rows || boardCol < 0 || boardCol >= cols {
                        return false
                    }
                    // 他のブロックとの衝突チェック
                    if gameBoard[boardRow][boardCol] != .gray.opacity(0.2) {
                        return false
                    }
                }
            }
        }
        return true
    }

    // テトリミノをボードに固定
    private func placeTetromino() {
        let shape = currentTetromino.shape
        for r in 0..<shape.count {
            for c in 0..<shape[r].count {
                if shape[r][c] == 1 {
                    let boardRow = currentTetromino.position.row + r
                    let boardCol = currentTetromino.position.col + c
                    if boardRow >= 0 && boardRow < rows && boardCol >= 0 && boardCol < cols {
                        gameBoard[boardRow][boardCol] = currentTetromino.color
                    }
                }
            }
        }
    }

    // 新しいテトリミノを生成
    private func spawnNewTetromino() {
        currentTetromino = nextTetromino
        currentTetromino.position = (row: 0, col: Int.random(in: 0...(cols - currentTetromino.shape[0].count)))
        nextTetromino = Tetromino.random()
    }

    // ラインが揃ったら消去し、スコアを加算
    private func clearLines() {
        var linesCleared = 0
        var newBoard = gameBoard
        
        for r in (0..<rows).reversed() {
            if !newBoard[r].contains(.gray.opacity(0.2)) { // ラインが全て埋まっている
                newBoard.remove(at: r)
                newBoard.insert(Array(repeating: .gray.opacity(0.2), count: cols), at: 0)
                linesCleared += 1
            }
        }
        
        if linesCleared > 0 {
            gameBoard = newBoard
            // スコア計算 (例: 1ライン100点、2ライン300点、3ライン500点、4ライン(テトリス)800点)
            switch linesCleared {
            case 1: score += 100
            case 2: score += 300
            case 3: score += 500
            case 4: score += 800
            default: break
            }
        }
    }

    // ゲームをリスタート
    private func restartGame() {
        gameBoard = Array(repeating: Array(repeating: .gray.opacity(0.2), count: cols), count: rows)
        currentTetromino = Tetromino.random()
        nextTetromino = Tetromino.random()
        score = 0
        gameOver = false
    }
}

#Preview {
    TetrisGameView()
} 

最後に

使ってみた感想ですが大変素晴らしくお金がかかりすぎる機能でしたね💰

  1. コードを自動生成する精度は高い
  2. 早く自動生成してくれる
  3. 別料金での契約が必要でチャット画面に、$50~選択するボタンが表示された!

AIのモデルで、Geminiだとコード生成の質が悪いのではと聞くのですが今回ゲームを作ってみたところすごく精度がよかったですね。間違った箇所もあったが修正すればなんとかなりました😅

使いすぎるとお財布に優しくないぐらいお金かかりすぎるので、おすすめはしません💸

Discussion