C48. 繪製西洋棋棋盤的 nested loop 練習

好想下棋XD

練習結果

  • 不懂的地方請AI給提示,不給解答的方式,慢慢嘗試。先透過for 迴圈 搭配if else來放置棋子,在近一步修改為用 for 迴圈來放置棋子。
  • 感覺可以拿來寫棋譜!?

練習項目

  • for in 迴圈:繪製棋盤以及棋子。
  • if else 條件語句:判斷棋格的顏色、棋子的位置。

修正簡化後

  • struct結構:建立結構儲存棋子的資訊(棋子的類型、顏色)。
  • 透過 for in 迴圈來執行 結構陣列,並且在棋盤上適當的位置放置棋子。

流程

一開始的想法是難道我要拉64個 UIImageView 的 IBOutlet 的 Array 的 Outlet Collection嗎?感覺位置一定很難拿捏,同時很難去處理流程。後來想想,我是要做一個靜態的西洋棋盤以及放置好的棋子,應該是可以由一個 UIView 去做設計才對:

  • 先設置一個 UIView(chessBoardView),尺寸為 320*320。(因為棋盤為8*8,這樣就等於一個格子為40*40,可被整除比較好計算 XD)
  • 將 UIView,分割為相等大小的 64 個格子,並且設置棋盤格子的顏色。
  • 依照棋盤格子來設置棋子大小,依序在棋盤位置上擺放相對應的棋子。
左至右 棋子(ChessPiece)、棋盤格子(chessSquareImageView)和棋盤View(chessBoardView)。

最後的結果補充:

  • 棋子(ChessPiece)是一個 struct,包含了棋子在棋盤上的位置(行和列)以及棋子的圖片。
  • 棋盤格子(chessSquareImageView)用於表示棋盤上的每個格子的視圖元素。它們的位置和大小根據行、列數和棋盤格的大小計算得出,並且根據行列數的和來決定背景顏色。
  • 棋盤的View(chessBoardView)是一個 UIView,是用於顯示整個棋盤和棋子的容器。

設置棋盤格子

  • 紅框:棋盤的View大小。
  • 藍框:棋盤格子的大小。
  • 建立棋盤的View:先拉一個UIView的 IBoutlet,用來作為棋盤。
    // 西洋棋棋盤的 View,用來添加 棋盤格子(chessSquareImageView)
@IBOutlet weak var chessBoardView: UIView!
  • 計算格子大小:棋盤一共有 8 x 8 個格子,所以我原先的UIView的長寬為(320*320)的四方形,同時可以被整除。
  • 把寬度(高度)除以8,得到棋盤格子的邊長。(因為棋盤是正方形的,所以這個邊長同時也是棋盤格子的寬度和高度。)
        // 計算棋盤格子 寬度 以及 高度 的大小(8 * 8),獲取邊長。
let squareSize = chessBoardView.frame.width / 8
  • 設置 for 迴圈:執行nested loop。
  • 主迴圈控制棋盤的每一行,子迴圈控制棋盤的每一列。
// 外層迴圈控制棋盤的行
for row in 0...7 {

// 內層迴圈控制棋盤每一行的每一列
for column in 0...7 {

// 依照當前的行和列數,計算出棋盤格的位置,創建一個具有該位置和大小的 UIImageView
let chessSquareImageView = UIImageView(frame: CGRect(x: squareSize * CGFloat(column), y: squareSize * CGFloat(row), width: squareSize, height: squareSize))

// 根據行、列數的和是 偶數 來決定棋盤格的背景色
if (row + column) % 2 == 0 {
chessSquareImageView.backgroundColor = UIColor.white
} else {
chessSquareImageView.backgroundColor = UIColor.black
}

// 將創建的棋盤格子 添加到 棋盤 View 上
chessBoardView.addSubview(chessSquareImageView)
}

}
  • chessSquareImageView 棋盤格子的位置(x,y),大小(width,height):會隨著迴圈(row、column)的執行,而改變每次當下該棋盤格子的位置。此外,每個格子大小都是相同的。
程式碼中,CGRect的四個參數分別代表了:

x: 該矩形左上角的x坐標。在這裡,它等於棋盤格的邊長乘以當前列的數字,即 squareSize * CGFloat(column)。
這是因為每一列的寬度是 squareSize,所以列數乘以格子寬度就可以得到該格子在x軸上的位置。

y: 該矩形左上角的y坐標。在這裡,它等於棋盤格的邊長乘以當前行的數字,即 squareSize * CGFloat(row)。
這是因為每一行的高度是squareSize,所以行數乘以格子高度就可以得到該格子在y軸上的位置。

width: 該矩形的寬度。在這裡,棋盤格的寬度等於 squareSize。
height: 該矩形的高度。在這裡,棋盤格的高度等於 squareSize。

因此,這段程式碼會創建一個UIImageView,其大小和位置正好可以填充棋盤上的一個格子。然後,將其添加到棋盤上。
  • if else (row + column) % 2 == 0 :西洋棋盤格子的規律是白黑彼此交錯。
1.對於任何白色的格子,其 行索引 和 列索引 的總和是一個偶數;
2.對於任何黑色的格子,其行索引和列索引之和則是一個奇數。
3.例如,左上角的格子(索引為0,0)是白色,而右上角的格子(索引為0,7)是黑色。

補充:格子的對應位置:

在二維的格子系統(如棋盤)中:

1. row表示行數,這對應到垂直方向或者說是Y軸的位置。
行數通常從上到下遞增,也就是說,第0行在最上方,行數越大,對應的格子位置越往下。

2. column表示列數,這對應到水平方向或者說是X軸的位置。
列數通常從左到右遞增,也就是說,第0列在最左邊,列數越大,對應的格子位置越往右。

因此,當在處理二維的格子系統(如棋盤、像素網格等)時,需要注意這個規則,以確保你能正確地處理和計算每個格子的位置。

我的錯誤:當初我設置迴圈執行次數為1…8,導致整個位置跑掉。

錯誤!
  • 我請 ChatGPT 透過表格執行4次:讓我理解正確的問題。
這與座標系統的運作方式有關。在電腦圖形學和許多繪圖相關的程式庫中,像是 UIKit,我們通常使用的是"左上角原點座標系"。

在這個系統中,左上角是 (0, 0) 點,向右走 x 值增加,向下走 y 值增加。
所以當你用 0 到 7 的範圍去做迴圈,你正確地從 (0,0) 開始,然後增加你的 x 與 y 值來設定你的棋盤格。這就是為什麼你的棋盤看起來正確。

然而,如果你用 1 到 8 的範圍去做迴圈,你實際上從 (1格子大小, 1格子大小) 開始。
所以你的棋盤看起來就像是向右與向下移動了一個單位,這就是為什麼棋盤位置跑掉的原因。

所以,為了適應這個座標系統,我們通常會從 0 開始數,而不是從 1 開始。
做成表格就好懂多了,剛好對應棋盤格式。

添加旗子到棋盤上 (一開始的做法 if else)

我一開始的作法為用if else 來判斷 row 以及 column 的執行次序來判斷,該放哪個棋子。但因為太多 if else ,我這邊只貼出黑色步兵以及黑色城堡的部分。

// 判斷棋子的擺放位置 (黑色棋子)
if row == 1 {
// 設定棋子的位置,讓它在對應的棋盤格中間
let pieceX = squareSize * CGFloat(column) + (squareSize - chessPieceSize) / 2
let pieceY = squareSize * CGFloat(row) + (squareSize - chessPieceSize) / 2

// 創建一個新的 UIImageView 來表示棋子,並設定它的位置和大小
let pieceImageView = UIImageView(frame: CGRect(x: pieceX, y: pieceY, width: chessPieceSize, height: chessPieceSize))

// 將這個 ImageView 的圖像設定為 black_pawn 棋子的圖片
pieceImageView.image = UIImage(named: "black_pawn")
// 將這個棋子的 ImageView 添加到棋盤的 View 中
chessBoardView.addSubview(pieceImageView)

} else if row == 0, (column == 0 || column == 7) {
// 設置棋子的ImageView的大小及位置
let pieceX = squareSize * CGFloat(column) + (squareSize - chessPieceSize) / 2
let pieceY = squareSize * CGFloat(row) + (squareSize - chessPieceSize) / 2

let pieceImageView = UIImageView(frame: CGRect(x: pieceX, y: pieceY, width: chessPieceSize, height: chessPieceSize))

pieceImageView.image = UIImage(named: "black_rook")
chessBoardView.addSubview(pieceImageView)

}

..........
  • 想法:棋盤的UIImageView 中再添加 棋子UIImageView。這個棋子 UIImageView 的大小和位置與棋盤格相同,並且圖像設置為對應的棋子圖像。
  • 設置棋子大小:因為我不想讓棋子與棋盤格子的大小相同,所以我這邊另外設置一個 chessPieceSize,為格子的0.7倍。
// 棋子的大小是棋盤格大小的 0.7 倍
let chessPieceSize = squareSize * 0.7
  • 黑色步兵:明顯可以看到黑色步兵在棋盤上是一整行,根據迴圈範圍0…7的順序,可以知道位於row == 1 。
紅框處為黑色步兵
  • 找出中心點(因為我的棋子縮小)找出棋盤格的左上角位置,然後再加上偏移量,使棋子的中心對齊棋盤格的中心。(如果棋子的大小正好等於棋盤格的大小,那麼不需要計算這個偏移量。)
// 設定棋子的位置,讓它在對應的棋盤格中間
let pieceX = squareSize * CGFloat(column) + (squareSize - chessPieceSize) / 2
let pieceY = squareSize * CGFloat(row) + (squareSize - chessPieceSize) / 2

squareSize * CGFloat(column) 和 squareSize * CGFloat(row)
是用來找出棋盤格的左上角的位置(也就是棋子的基本起始位置)。

而 (squareSize - chessPieceSize) / 2 是計算棋盤格中心與棋子大小的差異,
這個差值的一半就是棋子從棋盤格左上角開始需要偏移的距離,這樣才能讓棋子的中心對齊棋盤格的中心。
  • 建立 pieceImageView(棋子):
x: pieceX 和 y: pieceY 是 UIImageView 的左上角座標。
pieceX 和 pieceY 是我們根據棋盤的格子大小和棋子的大小計算出的棋子的中心位置,讓棋子能在對應的棋盤格子中居中。

width: chessPieceSize 和 height: chessPieceSize 是 UIImageView 的寬度和高度,就是棋子的大小。
  • pieceImageView 設置棋子的圖片,並且添加到棋盤(chessBoardView)中。
  • 黑色城堡:城堡有兩顆,分別置於棋盤的同一行的最兩側,也就是 row == 0 , (column == 0 || column == 7) 。
黑色城堡
  • 接下來就是依序執行if else 就可以完成了,但是因為程式碼重複的部分太多,所以又再次做了修改!

補充修正:逗號和或運算符(||)在邏輯運算中具有不同的意義和優先級。

// 原本寫法
if row == 0, column == 0 || column == 7

/* 這段代碼的意思是:如果 row 等於 0 和 column 等於 1,或者 column 等於 6,
則執行條件內的代碼。這裡可能會產生混淆,因為這句話可能被解釋為當 row 等於 0 且 column 等於 1,
或者當 column 等於 6 時,都會執行條件內的代碼。這可能並不是你想要的結果。*/
  • 逗號被用來分隔多個條件,並且所有條件都必須為真,這樣if語句的主體才會被執行。這就像是一個隱含的邏輯且運算(&&)。
  • 運算符(||)用於連接兩個條件,並且只要其中一個條件為真,if語句的主體就會被執行。
// 修改後
if row == 0, (column == 0 || column == 7)

/*只有當 row 等於 0 且 column 等於 1 或者 column 等於 6,才會進入條件區塊。
換句話說,它要求兩個條件同時成立:1) row 必須等於 0,以及 2) column 必須等於 1 或 6。

逗號 , 在這裡等同於邏輯 "和" 運算符 &&,而括號則確保了 column == 1 || column == 6 先於 ,
進行運算,也就是 column 等於 1 或等於 6 這個條件會先被評估。如果不加括號,則可能會出現歧義或者不符合你預期的結果。

所以在這裡,你可以理解為,row == 0 和 (column == 1 || column == 6) 是同等優先級的,
只是要求它們兩個都必須為真。*/

將程式簡化

  • 詢問ChatGPT在簡化程式碼時的思考邏輯順序:
簡化程式碼通常涉及到一些重構(refactoring)的技巧。
這包括將重複的程式碼抽象為函數或者方法,將數據結構抽象為物件或者數據模型,或者使用適當的設計模式來改善程式碼的結構。

在你的案例中,簡化的思考順序可能如下:

1. 識別重複的程式碼:在你的程式碼中,每次放置棋子時,都會計算棋子的位置並創建一個新的UIImageView。
這是一段重複的程式碼,可以被抽象為一個單獨的函式。

2. 抽象為函數:你可以創建一個新的函數,例如addChessPiece(chessBoardView:squareSize:chessPieceSize:row:column:image:),來封裝創建和添加棋子的程式碼。
這樣在需要放置棋子時,只需要調用這個函數即可。

3. 抽象數據結構:你有一個固定的棋子配置,這可以被抽象為一個數據結構。
在這個案例中,可以使用一個元組的陣列來表示棋子的配置。

4. 使用適當的數據結構:在這個案例中,column這個元組的元素既可以是單個整數,也可以是整數的陣列。

5. 簡化條件判斷:在放置棋子時,你的原始程式碼使用了多個if/else if條件來判斷棋子的種類和位置。

這只是一種可能的思考順序,具體的步驟可能會根據實際的需求和狀況來調整。
但是,重點是試圖找出重複的程式碼和數據結構,並嘗試將其抽象化,從而簡化程式碼並提高其可讀性和維護性。

struct 建立、建立實例陣列

  • 每個棋子都有特定的屬性:他們各自位於特定的行(row)和列(column),並且每個棋子都有對應的圖片(image)
  • 因此我將棋子的屬性組合在一起,而不是分散在代碼的各個地方。
// 建立棋子的struct
struct ChessPiece {

var row: Int // 棋子在棋盤上的行位置。
var columns: [Int] // 棋盤上的列位置,但有些棋子會有兩個位置。
var image: String // 棋子的圖片的名稱。
}
  • ChessPiece 結構體的實例陣列,每一個實例都代表一個棋子。
  • 每個棋子設定了相應的行(row)、列(columns)和圖片(image)。
  • 例如:黑色的兵,row 為 1,columns 為0…7,因為有8顆棋子。
        // 使用struct建立每個棋子資訊
let pieces = [
ChessPiece(row: 1, columns: Array(0...7), image: "black_pawn"), // 黑色的兵
ChessPiece(row: 0, columns: [0, 7], image: "black_rook"), // 黑色的城堡
ChessPiece(row: 0, columns: [1, 6], image: "black_knight"), // 黑色的騎士
ChessPiece(row: 0, columns: [2, 5], image: "black_bishop"), // 黑色的主教
ChessPiece(row: 0, columns: [3], image: "black_queen"), // 黑色的皇后
ChessPiece(row: 0, columns: [4], image: "black_king"), // 黑色的國王
ChessPiece(row: 6, columns: Array(0...7), image: "white_pawn"), // 白色的兵
ChessPiece(row: 7, columns: [0, 7], image: "white_rook"), // 白色的城堡
ChessPiece(row: 7, columns: [1,6], image: "white_knight"), // 白色的騎士
ChessPiece(row: 7, columns: [2, 5], image: "white_bishop"), // 白色的主教
ChessPiece(row: 7, columns: [3], image: "white_queen"), // 白色的皇后
ChessPiece(row: 7, columns: [4], image: "white_king"), // 白色的國王
]

添加棋子到指定位置上的 function

將原先每個棋子都會用到的設定棋子的中央位置以及將棋子的Image添加到棋盤中的程式碼整合成 fucntion。

  • chessBoardView:棋盤View,用於添加棋子。
  • squareSize:棋盤格的大小。
  • chessPieceSize:棋子的大小。
  • rowcolumn:要添加棋子的行和列。
  • image:棋子對應的圖片檔案名稱。
   // addChessPiece 指定的位置添加棋子
func addChessPiece(chessBoardView: UIView,squareSize: CGFloat, chessPieceSize: CGFloat, row:Int, column: Int, image: String ) {

// 設定棋子的位置,讓它在對應的棋盤格中間
let pieceX = squareSize * CGFloat(column) + (squareSize - chessPieceSize) / 2
let pieceY = squareSize * CGFloat(row) + (squareSize - chessPieceSize) / 2

// 創建一個新的 UIImageView 來表示棋子,並設定它的位置和大小
let pieceImageView = UIImageView(frame: CGRect(x: pieceX, y: pieceY, width: chessPieceSize, height: chessPieceSize))

// 將這個 ImageView 的圖像設定為 當前 棋子的圖片,名稱由參數 image 指定
pieceImageView.image = UIImage(named: image)
// 將這個棋子的 ImageView 添加到棋盤的 View 中
chessBoardView.addSubview(pieceImageView)

}

for in 迴圈來放置棋子

將原先用if else 來將每個棋子放置到位置上,改為用for 迴圈來遍歷我的 pieces 陣列,並且在棋盤上適當的位置放置棋子。

  • pieces 陣列中,每一個元素都代表一個棋子,包含該棋子應該出現的行 (row)、可能出現的列 (columns)、以及棋子的圖片 (image)。
  • for 迴圈中,檢查每一個棋盤格子的行 (row) 和列 (column) 是否符合當前棋子應該出現的位置。如果是,就調用 addChessPiece 方法在該棋盤格子上放置棋子。
                // 放置棋子
for piece in pieces {

// 循環每個棋子
if row == piece.row, piece.columns.contains(column) {

// 當目前的行和列符合某個棋子的位置,則放置該棋子
addChessPiece(chessBoardView: chessBoardView, squareSize: squareSize, chessPieceSize: chessPieceSize, row: row, column: column, image: piece.image)

}
}
  • row == piece.row:如果當前棋盤的行(row)和當前棋子(piece)的行(piece.row)相同,則條件成立。
  • piece.columns.contains(column):如果當前棋子(piece)的列(piece.columns)包含了當前棋盤的列(column),則條件成立。
  • 這兩個條件使用 , 相連,所以只有當這兩個條件都成立時,if 語句內的程式碼才會執行。也就是說,只有當當前的棋子位置(行和列)和當前的棋盤格子位置(行和列)完全一致時,才會在該棋盤格子上添加對應的棋子。
檢查當前的棋盤格子(row, column)是否符合當前棋子的位置(piece.row, piece.columns)。
如果棋盤格子的行數與棋子的行數相同,且棋盤格子的列數包含在棋子的列數中,則將棋子放置在這個棋盤格子上。

此部分程式碼的主要目的是在適當的棋盤格子上放置對應的棋子。
例如,如果當前的棋子是黑色的兵(ChessPiece(row: 1, columns: Array(0...7), image: "black_pawn")),
那麼這段程式碼會在棋盤的第二行的所有列上放置黑色的兵。

補充

一開始的錯誤
  • 這個錯誤表示二進位運算符 == 不能應用於類型為 Int[Int] 的運算元。這是因為 == 運算符用於比較兩個相同類型的值。
// 原本的寫法產生錯誤,這樣的寫法會將一個整數與可選陣列進行相等比較,會產生錯誤
column == piece.columns
  • 使用contains方法來檢查陣列是否包含特定的元素。contains方法可以用於檢查可選陣列的值是否存在,同時進行元素比較。
// 正確的寫法
piece.columns.contains(column)
  • 由於 piece.columns 是一個整數陣列 [Int],使用陣列的 contains 方法來檢查該陣列是否包含特定的整數。因此,正確的比較應該是 piece.columns.contains(column)

修改後的程式碼

import UIKit

class ViewController: UIViewController {

// 西洋棋棋盤的 View,用來添加 棋盤格子(chessSquareImageView)
@IBOutlet weak var chessBoardView: UIView!

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

// 計算每個棋盤格的大小(8 * 8)
let squareSize = chessBoardView.frame.width / 8

// 棋子的大小是棋盤格大小的 0.7 倍
let chessPieceSize = squareSize * 0.7

// 使用struct建立每個棋子資訊
let pieces = [
ChessPiece(row: 1, columns: Array(0...7), image: "black_pawn"), // 黑色的兵
ChessPiece(row: 0, columns: [0, 7], image: "black_rook"), // 黑色的城堡
ChessPiece(row: 0, columns: [1, 6], image: "black_knight"), // 黑色的騎士
ChessPiece(row: 0, columns: [2, 5], image: "black_bishop"), // 黑色的主教
ChessPiece(row: 0, columns: [3], image: "black_queen"), // 黑色的皇后
ChessPiece(row: 0, columns: [4], image: "black_king"), // 黑色的國王
ChessPiece(row: 6, columns: Array(0...7), image: "white_pawn"), // 白色的兵
ChessPiece(row: 7, columns: [0, 7], image: "white_rook"), // 白色的城堡
ChessPiece(row: 7, columns: [1,6], image: "white_knight"), // 白色的騎士
ChessPiece(row: 7, columns: [2, 5], image: "white_bishop"), // 白色的主教
ChessPiece(row: 7, columns: [3], image: "white_queen"), // 白色的皇后
ChessPiece(row: 7, columns: [4], image: "white_king"), // 白色的國王
]

// 外層迴圈控制棋盤的行
for row in 0...7 {

// 內層迴圈控制棋盤每一行的每一列
for column in 0...7 {

// 依照當前的行和列數,計算出棋盤格的位置,創建一個具有該位置和大小的 UIImageView
let chessSquareImageView = UIImageView(frame: CGRect(x: squareSize * CGFloat(column), y: squareSize * CGFloat(row), width: squareSize, height: squareSize))

// 根據行、列數的和是 偶數 來決定棋盤格的背景色
if (row + column) % 2 == 0 {
chessSquareImageView.backgroundColor = UIColor.white
} else {
chessSquareImageView.backgroundColor = UIColor.black
}

// 將創建的棋盤格子 添加到 棋盤 View 上
chessBoardView.addSubview(chessSquareImageView)

// 放置棋子
for piece in pieces {

// 循環每個棋子
if row == piece.row, piece.columns.contains(column) {

// 當目前的行和列符合某個棋子的位置,則放置該棋子
addChessPiece(chessBoardView: chessBoardView, squareSize: squareSize, chessPieceSize: chessPieceSize, row: row, column: column, image: piece.image)

}
}

}

}

}

// addChessPiece 指定的位置添加棋子
func addChessPiece(chessBoardView: UIView,squareSize: CGFloat, chessPieceSize: CGFloat, row:Int, column: Int, image: String ) {

// 設定棋子的位置,讓它在對應的棋盤格中間
let pieceX = squareSize * CGFloat(column) + (squareSize - chessPieceSize) / 2
let pieceY = squareSize * CGFloat(row) + (squareSize - chessPieceSize) / 2

// 創建一個新的 UIImageView 來表示棋子,並設定它的位置和大小
let pieceImageView = UIImageView(frame: CGRect(x: pieceX, y: pieceY, width: chessPieceSize, height: chessPieceSize))

// 將這個 ImageView 的圖像設定為 當前 棋子的圖片,名稱由參數 image 指定
pieceImageView.image = UIImage(named: image)
// 將這個棋子的 ImageView 添加到棋盤的 View 中
chessBoardView.addSubview(pieceImageView)

}
}

GitHub

參考

--

--

wei Tsao 學習紀錄
彼得潘的 Swift iOS / Flutter App 開發教室

Hi ! 我是wei , 先前未接觸過程式開發設計,想藉此來記錄自己的學習歷程,以利培養自己的程式邏輯 :)