Question: C#: Implement a multiplayer Battleship game with AI The rules are the same as before. The game is played on an NxN grid. Each player
C#: Implement a multiplayer Battleship game with AI
The rules are the same as before.
The game is played on an NxN grid.
Each player will place a specified collection of ships:
The ships will vary in length (size) from 2 to 5;
There can be any number or any size ship. There may be no ships of a particular size;
EXCEPT the battleship which there will always be 1 and only 1.
Player order will be random but fixed at the start of the game.
Each round consists of each player making a grid selection in turn.
Each grid selection will be played into all players grids including the current players grid.
Each player will respond with HIT, MISS, or SANK {ship name}.
If a players battleship is sunk that player is removed from the game.
Repeat from #4 until only 1 player remains.
That player is the winner.
This is what we are looking to fix:
In GetAttackPosition(): If it runs out of spots adjacent to hits it doesnt guess randomly until it finds a valid spot. As such, it wont stop avoiding its own ships after enough attempts (potential infinite loop).
In StartNewGame(): Doesnt reset members if more than 1 game is played during runtime.
In StartNewGame(): Doesnt place ships very intelligently. Right now it just borrows the logic from RandomPlayer.
DumbPlayer.cs
using System; using System.CodeDom; using System.Collections.Generic; using System.Runtime.InteropServices;
namespace CS3110_Module_8_GroupGREY { internal class DumbPlayer : IPlayer { private static int _nextGuess = 0; //Note static - we all share the same guess
private int _index; private int _gridSize;
public DumbPlayer(string name) { Name = name; } public void StartNewGame(int playerIndex, int gridSize, Ships ships) { _gridSize = gridSize; _index = playerIndex;
//DumbPlayer just puts the ships in the grid one on each row int y = 0; foreach (var ship in ships._ships) { ship.Place(new Position(0, y++), Direction.Horizontal); } }
public Position GetAttackPosition() { //A *very* naive guessing algorithm that simply starts at 0, 0 and guess each square in order //All 'DumbPlayers' share the counter so they won't guess the same one //But we don't check to make sure the square has not been guessed before var pos = new Position(_nextGuess % _gridSize, (_nextGuess /_gridSize)); _nextGuess++; return pos; }
public void SetAttackResults(List
public string Name { get; }
public int Index => _index; } }
GreyPlayer.cs
using System; using System.Collections.Generic;
namespace CS3110_Module_8_GroupGREY { internal class GreyPlayer : IPlayer { private List
// Constructor: public GreyPlayer(string name) { Name = name; }
// Property that returns player's name: public string Name { get; }
// Propery that reutn's player's indexin turn order. public int Index => _index;
// Logic to start new game // **** TBD **** // TBD: Does this properly reset if more than 1 game is played during runtime? // (arrays need to be reset, etc.) // **** TBD **** public void StartNewGame(int playerIndex, int gridSize, Ships ships) { _gridSize = gridSize; _index = playerIndex;
// **** TBD **** // TBD: Find a 'smarter' way to place ships. // Currently: this borrows from RandomPlayer, which just puts the ships in the grid in Random columns //Note it cannot deal with the case where there's not enough columns //for 1 per ship // **** TBD ****
var availableColumns = new List
_ships = ships; foreach (var ship in ships._ships) { // Pick an open X from the remaining columns var x = availableColumns[Random.Next(availableColumns.Count)]; availableColumns.Remove(x); //Make sure we can't pick it again
// Pick a Y that fits var y = Random.Next(gridSize - ship.Length); ship.Place(new Position(x, y), Direction.Vertical); } }
// Method to intelligently find best spot to atack. public Position GetAttackPosition() { Position guess = null;
// - (1) Look at the spaces to the north, east, south, or west of each hit (reference the HitPositions array here). // - (2) If the it finds a spot on the grid that doesnt contain the AIs own ships, it will shoot at it. foreach (Position position in HitPositions) { foreach (char direction in directions) { guess = GetAdjacent(position, direction); if (guess != null) break; } if (guess != null) break; }
// If guess is null by now, that means nothing has been found. // **** TBD **** // - (3) TBD: Otherwise, the AI will randomly select a space. If this space is on the grid, open, and dosn't contain the AIs // own ships, it will shoot at it. // - (4) TBD: Repeat (3) X amount of times. X scales based on grid size. // - (5) TBD: If still not shot has been taken, the AI will fire at any open spot at the grid, regardless of its own ships. // **** TBD **** if (guess == null) guess = new Position(0, 0); // ( This is a placeholder that just guesses 0, 0. )
return guess; }
// Method to find adjacent spot to a given position, if provided the direction. // Returns null if the spot is somehow invalid (off the grid or has already been shot at) internal Position GetAdjacent(Position p, char direction) { // initialize x & y int x = p.X; int y = p.Y;
// shift in the desired adjacent direction if (direction == 'N') y++; else if (direction == 'E') x++; else if (direction == 'S') y--; else if (direction == 'W') x--; else return null;
// save result Position result = new Position(x, y);
// return result if valid if (IsValid(result)) return result;
// return null otherwise else return null;
}
// This method, given a position, checks if it is a valid spot at which to fire. // Valid spots do not contain the player's own ships, have not already been shot at, and // are on the grid. internal bool IsValid(Position p) { // Check to see if spot contains the AI's ship. foreach (Ship s in _ships._ships) { foreach (Position ShipPosition in s.Positions) { if (ShipPosition.X == p.X && ShipPosition.Y == p.Y) { return false; } } }
// Check to see if spot has already been shot at foreach (List
// Check to see if spot is on the grid if (p.X < 0 || p.X >= _gridSize || p.Y < 0 || p.Y >= _gridSize) { return false; } // If all the checks have passed, this spot is valid. return true;
}
// Method to log results throughout the game. // GreyPlayer will separately keep track of each guess that results in a hit or a miss. // It does not track misses, as those require no follow up. public void SetAttackResults(List
RandomPlayer.cs
using System; using System.Collections.Generic;
namespace CS3110_Module_8_GroupGREY { internal class RandomPlayer : IPlayer { private static readonly List
public RandomPlayer(string name) { Name = name; }
public void StartNewGame(int playerIndex, int gridSize, Ships ships) { _gridSize = gridSize; _index = playerIndex;
GenerateGuesses();
//Random player just puts the ships in the grid in Random columns //Note it cannot deal with the case where there's not enough columns //for 1 per ship var availableColumns = new List
foreach (var ship in ships._ships) { //Choose an X from the set of remaining columns var x = availableColumns[Random.Next(availableColumns.Count)]; availableColumns.Remove(x); //Make sure we can't pick it again
//Choose a Y based o nthe ship length and grid size so it always fits var y = Random.Next(gridSize - ship.Length); ship.Place(new Position(x, y), Direction.Vertical); } }
private void GenerateGuesses() { //We want all instances of RandomPlayer to share the same pool of guesses //So they don't repeat each other.
//We need to populate the guesses list, but not for every instance - so we only do it if the set is missing some guesses if (Guesses.Count < _gridSize*_gridSize) { Guesses.Clear(); for (int x = 0; x < _gridSize; x++) { for (int y = 0; y < _gridSize; y++) { Guesses.Add(new Position(x,y)); } } } }
public string Name { get; } public int Index => _index;
public Position GetAttackPosition() { //RandomPlayer just guesses random squares. Its smart in that it never repeats a move from any other random //player since they share the same set of guesses //But it doesn't take into account any other players guesses var guess = Guesses[Random.Next(Guesses.Count)]; Guesses.Remove(guess); //Don't use this one again return guess; }
public void SetAttackResults(List
Grid.cs
using System;
namespace CS3110_Module_8_GroupGREY { public class Grid { private readonly GridEntry[,] _grid; private readonly int _gridSize;
public Grid(int gridSize) { _gridSize = gridSize; _grid = new GridEntry[gridSize,gridSize]; //Fill the grid with empty entries marked as not hit for (int x = 0; x < gridSize; x++) { for (int y = 0; y < gridSize; y++) { _grid[x,y] = new GridEntry(); } } }
public void Add(Ships ships) { foreach (var ship in ships._ships) { if (ship.Positions == null) { throw new ArgumentException("A player has not set the ships positions"); }
foreach (var pos in ship.Positions) { if (pos.X< 0 || pos.X >_gridSize || pos.Y <0 || pos.y>= _gridSize) { throw new ArgumentException("One of the ships is outside the grid"); }
if (pos.Hit) { throw new ArgumentException("One of the players is adding a hit ship to the game"); }
if (_grid[pos.X, pos.Y].Ship != null) { throw new ArgumentException("One of the players has an overlapping ship"); }
_grid[pos.X, pos.Y].Ship = ship; } } }
public void Draw(int drawX, int drawY) { for (int x = 0; x < _gridSize; x++) { for (int y = 0; y < _gridSize; y++) { Console.SetCursorPosition(drawX + x, drawY + y); Console.ForegroundColor = (_grid[x, y].Ship == null) ? ConsoleColor.Gray : _grid[x, y].Ship.Color; //Find if this segment of the ship is hit Console.BackgroundColor = (_grid[x,y].Hit)? ConsoleColor.Red : ConsoleColor.Black; if (_grid[x, y].Ship == null) { Console.Write("."); } else { Console.Write(_grid[x, y].Ship.Character); } } }
//Reset colors Console.BackgroundColor = ConsoleColor.Black; Console.ForegroundColor = ConsoleColor.White; }
public void Attack(Position pos) { _grid[pos.X, pos.Y].Hit = true; } } }
GridEntry.cs
namespace CS3110_Module_8_GroupGREY { public class GridEntry { public bool Hit; public Ship Ship; } }
IPlayer.cs
using System; using System.Collections.Generic;
namespace CS3110_Module_8_GroupGREY { interface IPlayer { ///
///
///
///
///
MultiplayerBattleship.cs
using System; using System.Collections.Generic; using System.Linq; using System.Threading;
namespace CS3110_Module_8_GroupGREY { internal class MultiPlayerBattleShip { const int GridSize = 10; //Your player should work when GridSize >=7
private static readonly Random Random = new Random();
private readonly List
private List
public MultiPlayerBattleShip(List
internal void Play(PlayMode playMode) { currentPlayers = new List
//Add each player in a random order for (int i = 0; i < _players.Count; i++) { var player = availablePlayers[Random.Next(availablePlayers.Count)]; availablePlayers.Remove(player); currentPlayers.Add(player); }
//Tell each player the game is about to start for (int i=0; i var count = ships._ships.Count(); int totalLength = ships._ships.Sum(ship => ship.Length); currentPlayers[i].StartNewGame(i, GridSize, ships); //Make sure player didn't change ships if (count != ships._ships.Count() || totalLength != ships._ships.Sum(ship => ship.Length)) { throw new Exception("Ship collection has ships added or removed"); } var grid = new Grid(GridSize); grid.Add(ships); _playerGrids.Add(grid); _playerShips.Add(ships); } int currentPlayerIndex = 0; while (currentPlayers.Count > 1) { var currentPlayer = currentPlayers[currentPlayerIndex]; //Ask the current player for their move Position pos = currentPlayer.GetAttackPosition(); //Work out if anything was hit var results = CheckAttack(pos); //Notify each player of the results foreach (var player in currentPlayers) { player.SetAttackResults(results); } DrawGrids(); Console.WriteLine(" Player " + currentPlayer.Index + "[" + currentPlayer.Name + "] turn."); Console.WriteLine(" Attack: " + pos.X + "," + pos.Y); Console.WriteLine(" Results:"); foreach (var result in results) { Console.Write(" Player " + result.PlayerIndex + " " + result.ResultType); if (result.ResultType == AttackResultType.Sank) { Console.Write(" - " + result.SunkShip); } Console.WriteLine(); } //Remove any ships with sunken battleships //Iterate backwards so that we don't mess with the indexes for (int i = currentPlayers.Count - 1; i >= 0; --i) { var player = currentPlayers[i]; if (_playerShips[player.Index].SunkMyBattleShip) { currentPlayers.Remove(player); //We never want to remvoe all the players... if (currentPlayers.Count == 1) { break; } } } //Move to next player wrapping around the end of the collection currentPlayerIndex = (currentPlayerIndex + 1)%currentPlayers.Count; if (playMode == PlayMode.Pause) { Console.WriteLine(" Press a key to continue"); Console.ReadKey(true); } else { Thread.Sleep(2000); } } Console.WriteLine(); Console.WriteLine("Winner is '" + currentPlayers[0].Name + "'"); Console.ReadKey(true); } private List foreach (var player in currentPlayers) { var result = _playerShips[player.Index].Attack(pos); //Mark attacks on the grid foreach (var grid in _playerGrids) { grid.Attack(pos); } result.PlayerIndex = player.Index; results.Add(result); } return results; } private void DrawGrids() { Console.Clear(); int drawX = 0; int drawY = 0; for (int i=0; i < currentPlayers.Count; i++) { var player = currentPlayers[i]; var playerIndex = player.Index; var grid = _playerGrids[playerIndex]; Console.SetCursorPosition(drawX, drawY); Console.ForegroundColor = ConsoleColor.Black; Console.BackgroundColor = ConsoleColor.White; Console.Write(player.Name); grid.Draw(drawX, drawY+1); drawX += GridSize + 4; if (drawX + GridSize > Console.BufferWidth) { drawY += GridSize + 5; drawX = 0; } } } } } PlayMode.cs namespace CS3110_Module_8_GroupGREY { public enum PlayMode { Delay, Pause, } } Program.cs using System.Collections.Generic; namespace CS3110_Module_8_GroupGREY { class Program { static void Main(string[] args) { List MultiPlayerBattleShip game = new MultiPlayerBattleShip(players); game.Play(PlayMode.Pause); } } }
Step by Step Solution
There are 3 Steps involved in it
Get step-by-step solutions from verified subject matter experts
