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 results) { //DumbPlayer does nothing with these results - its going to keep making dumb guesses }

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 HitPositions = new List(); // stores all hit guesses private List MissPositions = new List(); // stores all miss guesses private List SankPositions = new List(); // stores all sank guesses private int _index; // player's index in turn order private int _gridSize; // size of grid private Ships _ships; // size of grid private static readonly Random Random = new Random(); // used to randomize choices private char[] directions = { 'N', 'E', 'S', 'W' }; //represents north, east, south, west

// 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(); for (int i = 0; i < gridSize; i++) { availableColumns.Add(i); }

_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 LoggedPositions in new[] { HitPositions, MissPositions, SankPositions }) { foreach (Position LoggedPosition in LoggedPositions) { if (LoggedPosition.X == p.X && LoggedPosition.Y == p.Y) { return false; } } }

// 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 results) { foreach (AttackResult r in results) { if (r.ResultType == AttackResultType.Miss) { if (MissPositions.Contains(r.Position) == false) MissPositions.Add(r.Position); } else if (r.ResultType == AttackResultType.Hit) { if (HitPositions.Contains(r.Position) == false) HitPositions.Add(r.Position); } else if (r.ResultType == AttackResultType.Sank) { if (SankPositions.Contains(r.Position) == false) SankPositions.Add(r.Position); } } } } }

RandomPlayer.cs

using System; using System.Collections.Generic;

namespace CS3110_Module_8_GroupGREY { internal class RandomPlayer : IPlayer { private static readonly List Guesses = new List(); private int _index; private static readonly Random Random = new Random(); private int _gridSize;

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(); for (int i = 0; i < gridSize; i++) { availableColumns.Add(i); }

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 results) { //Random player does nothing useful with these results, just keeps on making random guesses } } }

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 { ///

/// Initializes the players are the start of a game and returns the positions of the ships /// Note: This method should be used to reset any AI state. It will be called once per game and each session might be multiple games /// You may also use this to generate new data structures for this game. The Test harness will handle checking for hits based on your /// returned value so it is up to you if and how you want to store the representation of your own grid /// /// What is the index of this player for this game - may change each game /// Size of the square grid - may change each game /// A list of Ships to provide positions for - may change each game. You should populate this collection with positions void StartNewGame(int playerIndex, int gridSize, Ships ships);

///

/// The name of this player - displayed in the UI /// String Name { get; }

///

/// The index of this player - it should return the index passed into the StartNewGame /// int Index { get; }

///

/// This is where you put the AI that chooses which square to target /// /// A position with an x, y coordinate Position GetAttackPosition();

///

/// The game will notify you of the results of each attack. /// /// A collection for each player still in the game /// You will get the index, the attack position and the result of the attack void SetAttackResults(List results); } }

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 _players;

private List _playerGrids; private List _playerShips; private List currentPlayers;

public MultiPlayerBattleShip(List players) { this._players = players; }

internal void Play(PlayMode playMode) { currentPlayers = new List(); var availablePlayers = new List(_players); _playerGrids = new List(); _playerShips = 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 CheckAttack(Position pos) { var results = new 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 players = new List(); players.Add(new DumbPlayer("Dumb 1")); players.Add(new DumbPlayer("Dumb 2")); players.Add(new DumbPlayer("Dumb 3")); players.Add(new RandomPlayer("Random 1")); players.Add(new RandomPlayer("Random 2")); players.Add(new RandomPlayer("Random 3")); players.Add(new RandomPlayer("Random 4")); players.Add(new RandomPlayer("Random 5")); players.Add(new GreyPlayer("Grey 1")); players.Add(new GreyPlayer("Grey 2")); players.Add(new GreyPlayer("Grey 3"));

MultiPlayerBattleShip game = new MultiPlayerBattleShip(players); game.Play(PlayMode.Pause); } } }

Step by Step Solution

There are 3 Steps involved in it

1 Expert Approved Answer
Step: 1 Unlock blur-text-image
Question Has Been Solved by an Expert!

Get step-by-step solutions from verified subject matter experts

Step: 2 Unlock
Step: 3 Unlock

Students Have Also Explored These Related Databases Questions!