using System.Collections.ObjectModel; using System.Security.Cryptography; using System.Runtime.Serialization; using QwirkleClassLibrary.Tiles; using QwirkleClassLibrary.Boards; using QwirkleClassLibrary.Events; using QwirkleClassLibrary.Players; using System.ComponentModel; using System.Runtime.CompilerServices; using static System.Formats.Asn1.AsnWriter; namespace QwirkleClassLibrary.Games { [DataContract] public class Game : IPlayer, IRules, INotifyPropertyChanged { [DataMember] private TileBag? bag = null; [DataMember] public bool GameRunning { get; set; } [DataMember] private Board board = new(17, 14); public bool PlayerSwapping { get; set; } public Board Board => board; public ReadOnlyCollection PlayerList => players.AsReadOnly(); [DataMember] private readonly List players = []; [DataMember] private readonly Dictionary scoreBoard = new Dictionary(); public ReadOnlyDictionary ScoreBoard => scoreBoard.AsReadOnly(); [DataMember] private readonly ObservableCollection> observableScoreBoard = []; public ReadOnlyObservableCollection> ObservableScoreBoard => new(observableScoreBoard); [DataMember] private readonly List cellUsed = []; public ReadOnlyCollection CellsUsed => cellUsed.AsReadOnly(); public event EventHandler? PlayerAddNotified; protected virtual void OnPlayerNotified(AddPlayerNotifiedEventArgs args) => PlayerAddNotified?.Invoke(this, args); public event EventHandler? NextPlayerNotified; protected virtual void OnNextPlayer(NextPlayerNotifiedEventArgs args) => NextPlayerNotified?.Invoke(this, args); public event EventHandler? PlaceTileNotified; protected virtual void OnPlaceTile(PlaceTileNotifiedEventArgs args) => PlaceTileNotified?.Invoke(this, args); public event EventHandler? EndOfGameNotified; protected virtual void OnEndOfGame(EndOfGameNotifiedEventArgs args) => EndOfGameNotified?.Invoke(this, args); public event EventHandler? SwapTilesNotified; public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } protected virtual void OnSwapTiles(SwapTilesNotifiedEventArgs args) => SwapTilesNotified?.Invoke(this, args); /// /// Adds a player in the game if the game is not running, if the name is correct, if the game is not full and if the name is not already taken. /// /// /// boolean to check it public bool AddPlayerInGame(List playersTag) { if (GameRunning) { OnPlayerNotified(new AddPlayerNotifiedEventArgs("ERROR : The game is running.")); return false; } for (int i = playersTag.Count - 1; i >= 0; i--) { if (string.IsNullOrWhiteSpace(playersTag[i])) { playersTag.RemoveAt(i); } } if (playersTag.Count <= 1 || playersTag.Count > 4) { playersTag.Clear(); OnPlayerNotified(new AddPlayerNotifiedEventArgs( "ERROR : It takes a minimum of 2 players and a maximum of 4 players to start a game.")); return false; } for (int i = playersTag.Count - 1; i >= 0; i--) { if (!CheckPlayerTag(playersTag, i)) { playersTag.RemoveAt(i); return false; } } foreach (var tag in playersTag) { Player pl = CreatePlayer(tag); players.Add(pl); SetScoreBoard(pl.NameTag, 0); } OnPlayerNotified(new AddPlayerNotifiedEventArgs("Players were correctly added.")); return true; } public bool CheckPlayerTag(List playersTag, int pos) { if (string.IsNullOrWhiteSpace(playersTag[pos])) { OnPlayerNotified(new AddPlayerNotifiedEventArgs("ERROR with " + (pos + 1) + " entry : The name is null or white space.")); return false; } for (int i = 0; i < playersTag.Count; i++) { if (i == pos) { continue; } if (playersTag[i] == playersTag[pos]) { OnPlayerNotified( new AddPlayerNotifiedEventArgs("ERROR with " + (pos + 1) + " entry : Name alreay taken")); return false; } } return true; } /// /// Creates a player with a name /// /// /// Player public Player CreatePlayer(string playerTag) { var player = new Player(playerTag); return player; } /// /// Returns the Board of the game /// /// Board public Board? GetBoard() { return board; } /// /// Returns the tile bag of the game /// /// public TileBag? GetTileBag() { return bag; } /// /// Creates a Board with a number of columns and rows /// /// Board public Board CreateBoard() { board = new Board(17, 14); return board; } /// /// Creates a bag of tiles with a number of sets of 36 tiles /// /// /// TileBag public TileBag CreateTileBag(int nbSet) { bag = new TileBag(nbSet); return bag; } /// /// Starts the game if there are at least 2 players and at most 4 players /// public void StartGame() { if (players.Count < 2 || players.Count >= 5) return; board = CreateBoard(); bag = CreateTileBag(3); GameRunning = true; } /// /// Adds a cell to the list of cells used by the player in his turn if the cell is not null /// /// public void AddCellUsed(Cell? c) { if (c != null) cellUsed.Add(c); } /// /// Empty the list of cells used by the player at the end of his turn /// public void EmptyCellUsed() { cellUsed.Clear(); } /// /// Gets the player who is currently playing /// /// Player /// public Player GetPlayingPlayer() { if (GetPlayingPlayerPosition() == -1) { throw new ArgumentException("No player currently playing !"); } return players[GetPlayingPlayerPosition()]; } /// /// Gets the position of the player who is currently playing /// /// int public int GetPlayingPlayerPosition() { for (int i = 0; i < players.Count; i++) { if (players[i].IsPlaying) { return i; } } return -1; } /// /// Returns the tile of the player who is currently playing at the position postile /// /// /// Tile public Tile TileOfPlayerWithPos(int postile) { return players[GetPlayingPlayerPosition()].Tiles[postile]; } /// /// Gives random picked tiles to the players at the beginning of the game /// public void GiveTilesToPlayers() { foreach (var p in players) { for (int j = 0; j < 6; j++) { if (bag != null && p.Tiles.Count < 6) { int val = RandomNumberGenerator.GetInt32(0, bag.TilesBag!.Count); p.AddTileToPlayer(bag.TilesBag[val]); bag.RemoveTileInBag(bag.TilesBag[val]); } } } } /// /// Sets the first player of the game at the beginning of the game /// /// string /// public string SetFirstPlayer(ReadOnlyCollection playingPlayers) { if (!GameRunning) throw new ArgumentException("Game is not running"); Player? startingPlayer = null; int maxGroupSize = 0; foreach (var player in players) { var colorGroups = player.Tiles.GroupBy(t => t.GetColor).Select(g => g.Count()); var shapeGroups = player.Tiles.GroupBy(t => t.GetShape).Select(g => g.Count()); int playerMaxGroupSize = Math.Max(colorGroups.Max(), shapeGroups.Max()); if (playerMaxGroupSize > maxGroupSize) { maxGroupSize = playerMaxGroupSize; startingPlayer = player; } } startingPlayer!.IsPlaying = true; OnNextPlayer(new NextPlayerNotifiedEventArgs(players[0])); return startingPlayer.NameTag; } /// /// Sets the next player of the game. If there's no current player, it sets the first player /// /// public string SetNextPlayer() { int i = GetPlayingPlayerPosition(); if (i == -1) { return SetFirstPlayer(PlayerList); } players[i].IsPlaying = false; players[(i + 1) % players.Count].IsPlaying = true; OnNextPlayer(new NextPlayerNotifiedEventArgs(players[(i + 1) % players.Count])); return players[GetPlayingPlayerPosition()].NameTag; } /// /// Allows the player to place a tile on the Board at a (x, y) position /// /// /// /// /// /// bool public bool PlaceTile(Player player, Tile tile, int x, int y) { if (PlayerSwapping) { OnPlaceTile(new PlaceTileNotifiedEventArgs(tile, "you are swapping, you can't place tile !")); return false; } if (!TileInbag(player, tile)) { OnPlaceTile(new PlaceTileNotifiedEventArgs(tile, "you can't play")); return false; } if (!IsMoveCorrect(tile, x, y, board!)) return false; if (board!.AddTileInCell(x, y, tile)) { AddCellUsed(board.GetCell(x, y)); return player.RemoveTileToPlayer(tile); } return false; } private static bool TileInbag(Player player, Tile tile) { return player.Tiles.Any(t => ReferenceEquals(t, tile)); } /// /// Allows a player to draw tiles from the bag as soon as he has less than 6 tiles /// /// /// public bool DrawTiles(Player player) { while (player.Tiles.Count < 6) { if (bag!.TilesBag!.Count == 0) { return false; } int val = RandomNumberGenerator.GetInt32(0, bag.TilesBag.Count); player.AddTileToPlayer(bag.TilesBag[val]); bag.RemoveTileInBag(bag.TilesBag[val]); } return true; } /// /// Allows a player to swap some of his tile with the ones in the bag if he can't play them /// /// /// /// bool public bool SwapTiles(Player player, List tilesToSwap) { if (cellUsed.Count != 0) { OnSwapTiles(new SwapTilesNotifiedEventArgs("You can't swap tiles after placing some !")); ReSwap(tilesToSwap); return false; } if (tilesToSwap.Count == 0) { OnSwapTiles(new SwapTilesNotifiedEventArgs("You must select at least one tile to swap !")); ReSwap(tilesToSwap); return false; } if (bag!.TilesBag!.Count < tilesToSwap.Count) { OnSwapTiles(new SwapTilesNotifiedEventArgs("Not enough tiles in the bag to swap !")); ReSwap(tilesToSwap); return false; } if (!DrawTiles(player)) { return false; } foreach (var t in tilesToSwap) { bag!.AddTileInBag(t); } return true; } private void ReSwap(List tilesToSwap) { foreach (var t in tilesToSwap) { players[GetPlayingPlayerPosition()].AddTileToPlayer(t); } } /// /// Extension of IsMoveCorrect to check beyond the surrounding cells of the cell where the tile is placed /// /// /// /// /// /// used to get the direction on the x axis /// used to get the direction on the y axis /// /// bool public bool CheckExtendedSurroundingCells(ref bool previousTilesFound, Tile tile, int x, int y, int dx, int dy, Board b) { for (int i = 1; i < 7; i++) { var extendedCell = b.GetCell(x + i * dx, y + i * dy); if (cellUsed.Count == 0) { previousTilesFound = true; } if (cellUsed.Contains(extendedCell)) { previousTilesFound = true; } if (extendedCell?.Tile == null) { break; } if (extendedCell.Tile.GetColor != tile.GetColor && extendedCell.Tile.GetShape != tile.GetShape) { OnPlaceTile(new PlaceTileNotifiedEventArgs(tile, " : Color / Shape does not match with the surrounding tiles !")); return false; } if (extendedCell.Tile.GetColor == tile.GetColor && extendedCell.Tile.GetShape == tile.GetShape) { OnPlaceTile(new PlaceTileNotifiedEventArgs(tile, " : Tile already placed on the same line / column !")); return false; } if (i == 6) { OnPlaceTile(new PlaceTileNotifiedEventArgs(tile, " : Row/Column already are 6 tiles long !")); return false; } } return true; } /// /// Extension of IsMoveCorrect to check if the tiles are on the same line /// /// /// /// /// /// bool public bool CheckTilesInLine(List cells, Board b, int x, int y) { if (cells.Count == 0) { return true; } var x1 = cells[0].GetX; var y1 = cells[0].GetY; if (cells.Count < 2 && (x1 == x || y1 == y)) { return true; } if (x1 != x && y1 != y) { return false; } var x2 = cells[1].GetX; var y2 = cells[1].GetY; if (x1 == x2) { return x == x1; } if (y1 == y2) { return y == y1; } return false; } public bool CheckWrongCompletedLines(Tile tile, int x, int y, int dx, int dy, Board b, ref List checkdoubles) { int nbTiles = 1; for (int i = 1; i < 7; i++) { var extendedCell = b.GetCell(x + i * dx, y + i * dy); var extendedCell2 = b.GetCell(x - i * dx, y - i * dy); if (extendedCell?.Tile == null && extendedCell2?.Tile == null) { break; } if (extendedCell?.Tile != null) { nbTiles++; foreach (var t in checkdoubles) { if (t.CompareTo(extendedCell.Tile) == 0) { return false; } } checkdoubles.Add(extendedCell.Tile); } if (extendedCell2?.Tile != null) { nbTiles++; foreach (var t in checkdoubles) { if (t.CompareTo(extendedCell2.Tile) == 0) { return false; } } checkdoubles.Add(extendedCell2.Tile); } } return nbTiles <= 6; } /// /// Main method to check if the move the player is trying to make is correct /// /// /// /// /// /// bool public bool IsMoveCorrect(Tile t, int x, int y, Board b) { bool previousTilesFound = false; var checkDoubles = new List(); if (!b.HasOccupiedCase()) { return true; } if (b.GetCell(x, y)!.Tile != null) { OnPlaceTile(new PlaceTileNotifiedEventArgs(t, " : Cell already used !")); } var surroundingCells = new List { b.GetCell(x + 1, y), b.GetCell(x - 1, y), b.GetCell(x, y + 1), b.GetCell(x, y - 1) }; foreach (var cell in surroundingCells) { if (cell?.Tile == null) { continue; } if (cell.Tile.GetColor != t.GetColor && cell.Tile.GetShape != t.GetShape) { OnPlaceTile(new PlaceTileNotifiedEventArgs(t, " : Colors / Shapes do not match with the surrounding tiles !")); return false; } if (cell.Tile.GetColor == t.GetColor && cell.Tile.GetShape == t.GetShape) { OnPlaceTile(new PlaceTileNotifiedEventArgs(t, " is already placed on the same line / column !")); return false; } var dx = cell.GetX - x; var dy = cell.GetY - y; if (!CheckExtendedSurroundingCells(ref previousTilesFound, t, x, y, dx, dy, b)) { return false; } if (!CheckWrongCompletedLines(t, x, y, dx, dy, b, ref checkDoubles)) { OnPlaceTile(new PlaceTileNotifiedEventArgs(t, " : You can't complete this line ! (More than 6 tiles / same tiles on the line)")); return false; } } if (!CheckTilesInLine(cellUsed, b, x, y)) { OnPlaceTile(new PlaceTileNotifiedEventArgs(t, "isn't on the same line as the ones previously placed !")); return false; } if (surroundingCells.All(cell => cell?.Tile == null)) { OnPlaceTile(new PlaceTileNotifiedEventArgs(t, " : You can't place a tile that isn't adjacent to another one !")); return false; } if (previousTilesFound) return true; OnPlaceTile(new PlaceTileNotifiedEventArgs(t, " : You must place your tile next / on the same line as the ones previously placed !")); return false; } /// /// Main method to get the score of the player after he played his turn /// /// /// /// /// int public int GetPlayerScore(Player player, ReadOnlyCollection cellsPlayed, Board b) { if (cellsPlayed.Count == 0) { return 0; } int score = cellsPlayed.Count; int nbCellsInLine = cellsPlayed.Count; if (cellsPlayed.Count == 6) { score += 6; } int cellsX = cellsPlayed[0].GetX; int cellsY = cellsPlayed[0].GetY; if (cellsPlayed.Count > 1) { foreach (var cell in cellsPlayed) { if (cellsX != cell.GetX && cellsX != -1) { cellsX = -1; } else if (cellsY != cell.GetY && cellsY != -1) { cellsY = -1; } } } else { cellsX = cellsY = -1; } score += cellsPlayed.Sum(cell => CalculateAdjacentScore(cell, b, cellsPlayed, cellsX, cellsY, ref nbCellsInLine)); if (nbCellsInLine == 6) { score += 6; } if (!scoreBoard.TryAdd(player.NameTag, score)) { scoreBoard.TryGetValue(player.NameTag, out int scoreold); SetScoreBoard(player.NameTag, score + scoreold); } return score; } /// /// Extension of GetPlayerScore to calculate the score of the player based on the adjacent cells /// /// /// /// /// /// /// /// int public int CalculateAdjacentScore(Cell cell, Board b, ReadOnlyCollection cellsPlayed, int cellsX, int cellsY, ref int nbCellsInLine) { int score = 0; var surroundingCells = new[] { b.GetCell(cell.GetX + 1, cell.GetY), b.GetCell(cell.GetX - 1, cell.GetY), b.GetCell(cell.GetX, cell.GetY + 1), b.GetCell(cell.GetX, cell.GetY - 1) }; var checkedSurroundingCells = new List(); foreach (var adjacentCell in surroundingCells) { if (adjacentCell?.Tile == null || cellsPlayed.Contains(adjacentCell) || checkedSurroundingCells.Contains(adjacentCell)) { continue; } int dx = adjacentCell.GetX - cell.GetX; int dy = adjacentCell.GetY - cell.GetY; score += CalculateLineScore(cellsPlayed, cell, new Tuple(dx, dy), b, new Tuple(cellsX, cellsY), ref nbCellsInLine); checkedSurroundingCells.Add(adjacentCell); } return score; } /// /// Extension of GetPlayerScore to calculate the score of the player based on the line/column of the adjacent cells /// /// /// /// /// /// /// /// int public int CalculateLineScore(ReadOnlyCollection cellsPlayed, Cell cell, Tuple direction, Board b, Tuple orientation, ref int nbCellsInLine) { int score = 0; for (int i = 1; i < 6; i++) { var extendedCell = b.GetCell(cell.GetX + i * direction.Item1, cell.GetY + i * direction.Item2); if (extendedCell?.Tile == null || cellsPlayed.Contains(extendedCell)) { break; } if (direction.Item1 != 0 && orientation.Item1 == -1 || direction.Item2 != 0 && orientation.Item2 == -1) { nbCellsInLine++; } score++; } if (direction.Item1 == 0 && orientation.Item1 == -1 && orientation.Item2 != -1 || direction.Item2 == 0 && orientation.Item2 == -1 && orientation.Item1 != -1) { score += 1; } return score; } /// /// Returns the list of the positions of the players who still have tiles in their bag /// /// List public List CheckTilesBag() { List playerTilesBagPos = []; if (bag!.TilesBag!.Count <= 12) { for (int i = 0; i < players.Count; i++) { if (players[i].Tiles.Count != 0) { playerTilesBagPos.Add(i); } } } return playerTilesBagPos; } /// /// Returns a boolean to check if the player can play a tile on the Board /// /// /// public bool CheckPlacementPossibilities(List playerTilesBagPos) { foreach (var t1 in playerTilesBagPos) { foreach (var t in players[t1].Tiles) { for (int b = 0; b < board!.ReadCells.Count; b++) { int x = board.ReadCells[b].GetX; int y = board.ReadCells[b].GetY; if (IsMoveCorrect(t, x, y, board)) { return true; } } } } return false; } /// /// Main method to check if the game is over /// /// /// public bool CheckGameOver(Player player) { List playerTilesBagPos = CheckTilesBag(); if (playerTilesBagPos.Count != 0 && !CheckPlacementPossibilities(playerTilesBagPos) || bag!.TilesBag!.Count == 0 && players[GetPlayingPlayerPosition()].Tiles.Count == 0) { OnEndOfGame(new EndOfGameNotifiedEventArgs(player)); GameRunning = false; scoreBoard.TryGetValue(player.NameTag, out int scoreold); SetScoreBoard(player.NameTag, 6 + scoreold); return true; } return false; } public void ClearGame() { players.Clear(); scoreBoard.Clear(); cellUsed.Clear(); observableScoreBoard.Clear(); bag = null; board = CreateBoard(); GameRunning = false; } public void SetScoreBoard(string name, int score) { if (!scoreBoard.TryAdd(name, score)) { scoreBoard[name] = score; } observableScoreBoard.Clear(); foreach (var item in scoreBoard) { observableScoreBoard.Add(item); } OnPropertyChanged(nameof(ObservableScoreBoard)); } } }