Unit UI of BattleshipGames

by Josh Blake: L6 Age ~17

unit UI;
{
    Copyright (c) 2014 Josh Blake

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface
uses
  Game,
  Grid,
  SysUtils,
  Settings,
  StrUtils,
  Crt;

type
  TModes = Array[1..6] of String;

  TBaseUI = class
    private
      TwoGames: Boolean;   
      procedure DisplayTwoGames; virtual; abstract;
      procedure DisplayOneGame; virtual; abstract;
    protected
      Game1, Game2: TGame;
      Name1, Name2: String;
      function NumToLetter(Num: Integer): Char;
      function LetterToNum(Letter: Char): Integer;
      function CoordToString(Coord: TCoord): String;
      function ResultToString(GuessResult: TGuessResult): String;
    public
      class function DisplayMenu: Integer; virtual; abstract;
      constructor Create(aGame: TGame; PlayerName: String); overload;
      constructor Create(aGame1, aGame2: TGame; aName1, aName2: String); overload;
      procedure DisplayGame;
      function GetUserGuess(Game: TGame): TCoord; virtual; abstract;
      procedure ShowMessage(aMessage: string; wait: boolean = True); virtual; abstract;
      function PlaceUserShip(Grid: TGrid; Size: Integer): TChoice; virtual; abstract;
    end;

  ConsoleUI = class(TBaseUI)
    private
      procedure DisplayGrid(Grid: TGrid);
      procedure DisplayHiddenGrid(Grid: TGrid);
      procedure DisplayTwoGames; override;
      procedure DisplayOneGame; override;
      procedure PrintResult(Game: TGame; Name: String);
    public
      function GetUserGuess(Game: TGame): TCoord; override;
      function PlaceUserShip(Grid: TGrid; Size: Integer): TChoice; override;
      class function DisplayMenu: Integer; override;
      procedure ShowMessage(aMessage: string; wait: boolean = True); override;
  end;

implementation

const MODES: TModes = ('Just Guess', 'Watch the AI play',
                       'Play against the computer', 'Two player', 'Help', 'Exit');

constructor TBaseUI.Create(aGame: TGame; PlayerName: String);
begin
   Game1 := aGame;
   Name1 := PlayerName;
   TwoGames := False;
end;

constructor TBaseUI.Create(aGame1, aGame2: TGame; aName1, aName2: String);
begin
  Game1 := aGame1;
  Game2 := aGame2;
  Name1 := aName1;
  Name2 := aName2;
  TwoGames := True;
end;

procedure TBaseUI.DisplayGame;
begin
  if TwoGames then begin
    DisplayTwoGames;
  end else begin
    DisplayOneGame;
  end;
end;

function TBaseUI.LetterToNum(Letter: Char): Integer;
var
  Num: Integer;
begin
  Num := Ord(Letter) - 97;
  if Num < 0 then Num := Ord(Letter) - 65;   //maybe it's uppercase
  if (Num < 0) or (Num > 26) then Num := -1; //invalid
  LetterToNum := Num;
end;

function TBaseUI.NumToLetter(Num: Integer): Char;
begin
  NumToLetter := chr(Num + 65);
end;

function TBaseUI.CoordToString(Coord: TCoord): String;
begin
  Result := NumToLetter(Coord.X) + IntToStr(Coord.Y + 1);
end;

function TBaseUI.ResultToString(GuessResult: TGuessResult): String;
begin
  case GuessResult of
    Invalid: Result := 'entered an invalid value';
    Miss: Result := 'missed';
    Hit: Result := 'hit';
    Sunk: Result := 'sunk a ship';
  end;
end;

procedure ConsoleUI.DisplayHiddenGrid(Grid: TGrid);
var
  X, Y: Integer;
begin
  write('   ');
  //Print column names
  for X := 0 to Length(Grid) - 1 do begin
    write(NumToLetter(X) + ' ');
  end;
  writeln;  //new line
  
  for Y := 0 to Length(Grid) - 1 do begin
    Write(Y+1, ' ');
    if Y < 9 then write(' ');   //1 digit numbers need extra spacing
    for X := 0 to Length(Grid[Y]) - 1 do begin
      if Grid[X][Y].Guessed then begin  //Square has been guessed
        if Grid[X][Y].Content.CheckFilled then write('X ')
        else write('0 ');
      end else begin
        write('- ');  //Not guessed this square yet
      end;
    end;
    writeln; //Move to next line
  end;
  Writeln('Key: - = not guessed, 0 = miss, X = hit');
end;

procedure ConsoleUI.DisplayGrid(Grid: TGrid);
var
  X, Y: Integer;
begin
  write('   ');
  //Print column names
  for X := 0 to Length(Grid) - 1 do begin
    write(NumToLetter(X) + ' ');
  end;
  writeln;  //new line
  
  for Y := 0 to Length(Grid) - 1 do begin
    Write(Y+1, ' ');
    if Y < 9 then write(' ');   //1 digit numbers need extra spacing
    for X := 0 to Length(Grid[Y]) - 1 do begin
      if Grid[X][Y].Guessed then begin  //Square has been guessed
        if Grid[X][Y].Content.CheckFilled then write('X ')
        else write('0 ');
      end else begin
        if Grid[X][Y].Content.CheckFilled then write('| ')
        else write('- ');
      end;
    end;
    writeln; //Move to next line
  end;
  Writeln('Key: - = sea, | = enabled ship, 0 = been guessed, X = destroyed ship');
end;

procedure ConsoleUI.PrintResult(Game: TGame; Name: string);
begin
  if Game.LastGuessResult <> None then begin
    Writeln(Name, ' last guessed ', CoordToString(Game.LastGuess),
            '. ', Name, ' ', ResultToString(Game.LastGuessResult));
  end;
end;

procedure ConsoleUI.DisplayTwoGames;
begin
  ClrScr;
  //Display first grid
  Writeln(Name1, ':');
  if Game1.Hidden then DisplayHiddenGrid(Game1.Grid)
  else DisplayGrid(Game1.Grid);
  //Display second grid
  Writeln(Name2, ';');
  if Game2.Hidden then DisplayHiddenGrid(Game2.Grid)
  else DisplayGrid(Game2.Grid);
  //Show last guesses
  PrintResult(Game1, Name1);
  PrintResult(Game2, Name2);
end;

procedure ConsoleUI.DisplayOneGame;
begin
  ClrScr;
  if Game1.Hidden then DisplayHiddenGrid(Game1.Grid)
  else DisplayGrid(Game1.Grid);
  PrintResult(Game1, Name1);
end;

function ConsoleUI.GetUserGuess(Game: TGame): TCoord;
var
  UserGuess: String;
  Guess: TCoord;
  ErrorCode: Integer;
begin
  ClrScr;
  DisplayGame;
  while true do begin
    Writeln('Please enter your guess');
    readln(UserGuess);
    if (UserGuess = 'debug') and DEBUG then begin
      Writeln('Debugging');
      DisplayHiddenGrid(Game.Grid);
      Continue;
    end;
    if Length(UserGuess) < 2 then
      continue;  //Not long enough
    Guess.X := LetterToNum(UserGuess[1]);
    if Guess.X = -1 then begin
      Writeln('Column should be a letter');
      continue
    end;
    Val(RightStr(UserGuess, Length(UserGuess) - 1), Guess.Y, ErrorCode); //Check row is numeric
    if ErrorCode <> 0 then begin
      writeln('Row should be a number');
      Continue;
    end;
    Break;
  end;
  Dec(Guess.Y); //use 1 based in UI but 0 based internally
  Result := Guess;
end;

function ConsoleUI.PlaceUserShip(Grid: TGrid; Size: Integer): TChoice;
var
  Input, RowStr: String;
  GridMin, GridMax, ErrorCode: Integer;
  Valid: Boolean;
begin
  ClrScr;
  GridMin := Low(Grid);
  GridMax := High(Grid);
  DisplayGrid(Grid);
  Writeln('Ship of size ', Size, ' needs placing');
  Writeln('Please where you want the ship to start (either the top or left)');
  repeat
    Valid := False;
    readln(Input);
    if Length(Input) < 2 then begin  //Not long enough
      Writeln('No square entered');
      continue;
    end;
    Result.Start.X := LetterToNum(Input[1]);
    if Result.Start.X = -1 then begin
      Writeln('Column should be a letter');
      continue;
    end;
    RowStr := RightStr(Input, Length(Input) - 1);
    Val(RowStr, Result.Start.Y, ErrorCode); //Check row is numeric
    if ErrorCode <> 0 then begin
      writeln('Row should be a number');
      continue;
    end;
    Dec(Result.Start.Y);

    if (Result.Start.X < GridMin) or (Result.Start.X > GridMax) then begin
      Writeln('Invalid column entered');
      continue;
    end;
    if (Result.Start.Y < GridMin) or (Result.Start.Y > GridMax) then begin
      Writeln('Invalid row', Result.Start.Y, 'entered');
      continue;
    end;
    Valid := True;
  until Valid;

  Writeln('Choose direction: v = vertical (down), h = horizontal (across)');
  repeat
    Readln(Input);
  until (Input = 'v') or (Input = 'h');
  Result.ChangeX := (Input = 'h');
  if Result.ChangeX and (Result.Start.X + Size - 1 > GridMax) then begin
    Writeln('That would make the ship out of the grid! (Press enter to retry.)');
    Readln;
    PlaceUserShip(Grid, Size); //try again
  end;
  if (not Result.ChangeX) and (Result.Start.Y + Size - 1 > GridMax) then begin
    Writeln('That would make the ship out of the grid! (Press enter to retry.)');
    Readln;
    PlaceUserShip(Grid, Size); //try again
  end;
end;

class function ConsoleUI.DisplayMenu: Integer;
var
  I, ErrorCode: Integer;
  Input: String;
begin

  ClrScr;
  writeln('Chose mode, options are:');
  for I := Low(MODES) to High(Modes) do begin
    Writeln(I, ' = ', MODES[I]);
  end;
  Repeat
    readln(Input);
    Val(Input, Result, ErrorCode);
  until (ErrorCode = 0) and (Result >= Low(MODES)) and (Result <= High(MODES));
end;

procedure ConsoleUI.ShowMessage(aMessage: string; wait: boolean = True);
begin
  Writeln(aMessage);
  if wait then Readln;
end;

end.
Programming - a skill for life!

by Josh Blake: L6 Age ~17