C# WinForms海战游戏Bug:无法放置多段舰船的相邻船段
海战游戏舰船放置Bug排查
问题场景
我用C# Windows Forms开发海战游戏,流程如下:
- 程序启动后弹出对战模式选择对话框:玩家对战玩家、玩家对战电脑
- 选择模式后,选择网格尺寸:4x4、8x8、10x10
- 进入舰船放置界面
以4x4网格为例,需放置1艘尺寸1的舰船和2艘尺寸2的舰船。操作中遇到的问题:
- 成功将尺寸1的舰船放在坐标(i=0,j=0,i为列、j为行)
- 成功放置尺寸2舰船的第一段在(2,0)
- 尝试在(2,1)或(3,0)放置第二段时,均提示“无法在此位置放置船段,请重试”
已花费1小时排查仍未找到Bug根源,相关代码如下:
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace oop_template { public partial class ShipPlacementForm : Form { private int tableSize; private List<Ship> ships; private List<Ship> placedShips; private Button[,] tableButtons; private int currentShipIndex; private Ship currentShip; private bool horizontalPlacement; public List<Ship> PlacedShips { get { return placedShips; } } public ShipPlacementForm(int tableSize, List<Ship> ships) { this.tableSize = tableSize; this.ships = ships; this.placedShips = new List<Ship>(); CustomInitializeComponent(); // Initialize placement of the first ship this.currentShipIndex = 0; SetCurrentShip(); deleteButton.Enabled = false; // Disable delete until at least one ship is placed } private void CustomInitializeComponent() { this.SuspendLayout(); // Dynamically create buttons for the table fields tableButtons = new Button[tableSize, tableSize]; for (int i = 0; i < tableSize; i++) { for (int j = 0; j < tableSize; j++) { tableButtons[i, j] = new Button(); tableButtons[i, j].Size = new Size(30, 30); tableButtons[i, j].Location = new Point(30 * i, 30 * j); tableButtons[i, j].Click += new EventHandler(TableButton_Click); this.Controls.Add(tableButtons[i, j]); } } // Label for displaying the current ship this.currentShipLabel = new Label(); this.currentShipLabel.Location = new Point(30 * tableSize, 30); this.currentShipLabel.AutoSize = true; this.Controls.Add(this.currentShipLabel); // Button for confirmation this.confirmButton = new Button(); this.confirmButton.Text = "Confirm"; this.confirmButton.Location = new Point(30 * tableSize, 60); this.confirmButton.Click += new EventHandler(ConfirmButton_Click); this.confirmButton.Enabled = false; // Disabled until all ships are placed this.Controls.Add(this.confirmButton); // Button for deleting a ship this.deleteButton = new Button(); this.deleteButton.Text = "Delete Ship"; this.deleteButton.Location = new Point(30 * tableSize, 90); this.deleteButton.Click += new EventHandler(DeleteButton_Click); this.deleteButton.Enabled = false; // Disabled until at least one ship is placed this.Controls.Add(this.deleteButton); // Set form size this.ClientSize = new System.Drawing.Size(30 * (tableSize + 2), 30 * tableSize); this.Name = "ShipPlacementForm"; this.Text = "Ship Placement"; this.ResumeLayout(false); this.PerformLayout(); } private void TableButton_Click(object sender, EventArgs e) { Button clickedButton = sender as Button; if (clickedButton == null) return; int x = clickedButton.Location.X / 30; int y = clickedButton.Location.Y / 30; if (CanPlaceShipPart(x, y)) { PlaceShipPart(x, y); currentShip.Positions.Add(new Point(x, y)); if (currentShip.Positions.Count == 1) { // First part of the ship is placed, wait for the second part to determine orientation deleteButton.Enabled = false; // Disable deletion until the entire ship is placed } else if (currentShip.Positions.Count == 2) { // Determine the orientation of the ship based on the first two positions horizontalPlacement = currentShip.Positions[0].Y == currentShip.Positions[1].Y; } // Check if the ship is complete if (currentShip.Positions.Count == currentShip.Size) { placedShips.Add(currentShip); currentShipIndex++; if (currentShipIndex < ships.Count) { SetCurrentShip(); } else { currentShipLabel.Text = "All ships are placed."; confirmButton.Enabled = true; deleteButton.Enabled = true; } } } else { MessageBox.Show("You cannot place a ship part on this position. Please try again."); } } private void DeleteButton_Click(object sender, EventArgs e) { currentShipLabel.Text = "Click on any part of the ship you want to delete."; foreach (var ship in placedShips) { foreach (var position in ship.Positions) { tableButtons[position.X, position.Y].Click += DeleteShipClick; } } } private void DeleteShipClick(object sender, EventArgs e) { Button clickedButton = sender as Button; if (clickedButton == null) return; int x = clickedButton.Location.X / 30; int y = clickedButton.Location.Y / 30; Ship shipToDelete = null; foreach (var ship in placedShips) { if (ship.Positions.Contains(new Point(x, y))) { shipToDelete = ship; break; } } if (shipToDelete != null) { DeleteShip(shipToDelete); currentShipLabel.Text = $"Place a ship of size {shipToDelete.Size}"; currentShip = shipToDelete; currentShipIndex = ships.IndexOf(shipToDelete); foreach (var position in shipToDelete.Positions) { tableButtons[position.X, position.Y].Click -= DeleteShipClick; } } deleteButton.Enabled = placedShips.Count > 0; } private void DeleteShip(Ship ship) { foreach (var position in ship.Positions) { tableButtons[position.X, position.Y].BackColor = default(Color); tableButtons[position.X, position.Y].Enabled = true; } placedShips.Remove(ship); ships.Insert(currentShipIndex, ship); // Return the ship to the list for re-placement } private void ConfirmButton_Click(object sender, EventArgs e) { this.DialogResult = DialogResult.OK; this.Close(); } private void SetCurrentShip() { currentShip = ships[currentShipIndex]; currentShip.Positions.Clear(); currentShipLabel.Text = $"Place a ship of size {currentShip.Size}"; deleteButton.Enabled = placedShips.Count > 0; } private bool CanPlaceShipPart(int x, int y) { if (tableButtons[x, y].BackColor == Color.Gray) return false; // The field is already taken // Check continuity of ship placement if (currentShip.Positions.Count > 0) { var lastPos = currentShip.Positions[currentShip.Positions.Count - 1]; // Check if the second part of the ship can be placed next to the first one bool validHorizontalPos = (x == lastPos.X && Math.Abs(y - lastPos.Y) == 1); bool validVerticalPos = (y == lastPos.Y && Math.Abs(x - lastPos.X) == 1); if (validHorizontalPos || validVerticalPos) { // Check if the adjacent field is taken foreach (var (dx, dy) in new[] { (-1, 0), (1, 0), (0, -1), (0, 1) }) { int nx = x + dx; int ny = y + dy; if (nx >= 0 && nx < tableSize && ny >= 0 && ny < tableSize) { if (tableButtons[nx, ny].BackColor == Color.Gray) return false; // The adjacent field is taken } } return true; } else { return false; } } return true; } private void PlaceShipPart(int x, int y) { tableButtons[x, y].BackColor = Color.Gray; // Color the field gray tableButtons[x, y].Enabled = false; // Disable clicking on the same field again } private Label currentShipLabel; private Button confirmButton; private Button deleteButton; } }
Bug根源分析
问题出在CanPlaceShipPart方法的相邻区域检查逻辑:
当放置第二艘船的第二段时,代码会检查当前点击位置的四个相邻格子是否有已放置的灰色船段。而你之前在(0,0)放置了尺寸1的船,当尝试在(3,0)放置第二段时,代码会检查(3,0)的所有相邻格,包括左边的(2,0)(当前船的第一段,已标记为灰色),误将同一艘船的已放部分当成“已占用的外部区域”,直接返回false导致无法放置。
修复方案
修改CanPlaceShipPart方法,在检查相邻格子时,排除当前正在放置的船的已存位置:
private bool CanPlaceShipPart(int x, int y) { if (tableButtons[x, y].BackColor == Color.Gray) return false; // 当前格子已被占用 // 检查是否是同一艘船的连续放置 if (currentShip.Positions.Count > 0) { var lastPos = currentShip.Positions[currentShip.Positions.Count - 1]; // 检查是否与上一段相邻 bool validHorizontalPos = (x == lastPos.X && Math.Abs(y - lastPos.Y) == 1); bool validVerticalPos = (y == lastPos.Y && Math.Abs(x - lastPos.X) == 1); if (!validHorizontalPos && !validVerticalPos) { return false; // 不连续,无法放置 } // 检查相邻格子是否被其他船占用(排除当前船已放的位置) foreach (var (dx, dy) in new[] { (-1, 0), (1, 0), (0, -1), (0, 1) }) { int nx = x + dx; int ny = y + dy; if (nx >= 0 && nx < tableSize && ny >= 0 && ny < tableSize) { // 跳过当前船已放置的位置 if (currentShip.Positions.Any(p => p.X == nx && p.Y == ny)) continue; if (tableButtons[nx, ny].BackColor == Color.Gray) return false; // 相邻格子被其他船占用 } } return true; } else { // 放置第一块时,检查周围是否有其他船 foreach (var (dx, dy) in new[] { (-1, 0), (1, 0), (0, -1), (0, 1) }) { int nx = x + dx; int ny = y + dy; if (nx >= 0 && nx < tableSize && ny >= 0 && ny < tableSize) { if (tableButtons[nx, ny].BackColor == Color.Gray) return false; // 周围有其他船,无法放置 } } return true; } }
另外,补充修复一个潜在问题:Ship.Positions.Contains(new Point(x,y))依赖Point的引用比较,可能导致判断错误,建议在DeleteShipClick方法中替换为坐标值比较:
// 替换原判断逻辑 if (ship.Positions.Any(p => p.X == x && p.Y == y)) { shipToDelete = ship; break; }
内容的提问来源于stack exchange,提问作者Огњен Стојановић
相关产品推荐
相关产品推荐

