init
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ActionBlocks.Game\ActionBlocks.Game.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,87 @@
|
||||
using ActionBlocks.Game.Core;
|
||||
using static ActionBlocks.Game.Tests.TestHelpers;
|
||||
|
||||
namespace ActionBlocks.Game.Tests;
|
||||
|
||||
public class CoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void T_piece_rotates_clockwise_to_point_right()
|
||||
{
|
||||
var cells = PieceDefinitions.Minos(PieceType.T, 1).ToHashSet();
|
||||
Assert.Equal(new HashSet<Point> { new(1, 0), new(1, 1), new(2, 1), new(1, 2) }, cells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void I_piece_vertical_state_uses_third_column()
|
||||
{
|
||||
var cells = PieceDefinitions.Minos(PieceType.I, 1);
|
||||
Assert.All(cells, c => Assert.Equal(2, c.X));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Four_rotations_return_to_spawn_shape()
|
||||
{
|
||||
foreach (var type in PieceDefinitions.All)
|
||||
{
|
||||
Assert.Equal(PieceDefinitions.Minos(type, 0), PieceDefinitions.Minos(type, 4));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Seven_bag_deals_every_shape_once_per_bag()
|
||||
{
|
||||
var randomizer = new SevenBagRandomizer(new Random(42));
|
||||
for (var bag = 0; bag < 3; bag++)
|
||||
{
|
||||
var pieces = Enumerable.Range(0, 7).Select(_ => randomizer.Next()).ToHashSet();
|
||||
Assert.Equal(7, pieces.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rotation_against_wall_uses_kick()
|
||||
{
|
||||
var board = new Board(10, 20, 20);
|
||||
var srs = new SuperRotationSystem();
|
||||
// Vertical I hugging the left wall (column 0 in state L uses box column 1).
|
||||
var piece = new ActivePiece(new PieceSpec(PieceType.I), -1, 30, 3);
|
||||
Assert.True(board.Fits(piece));
|
||||
|
||||
var result = srs.TryRotate(board, piece, 1);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Value.KickIndex > 0);
|
||||
Assert.True(board.Fits(result.Value.Piece));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clearing_rows_returns_contained_items_and_drops_rows_above()
|
||||
{
|
||||
var board = new Board(10, 20, 20);
|
||||
var bottom = Bottom(board);
|
||||
FillRow(board, bottom);
|
||||
board[3, bottom] = new Cell(CellColor.Garbage, "nuke");
|
||||
board[0, bottom - 1] = new Cell(1);
|
||||
|
||||
var items = board.ClearRows(board.FullRows().ToList());
|
||||
|
||||
Assert.Equal(["nuke"], items);
|
||||
Assert.False(board[0, bottom].IsEmpty);
|
||||
Assert.Equal(1, FilledCount(board));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Garbage_pushes_rows_up_with_a_hole()
|
||||
{
|
||||
var board = new Board(10, 20, 20);
|
||||
board[5, Bottom(board)] = new Cell(1);
|
||||
|
||||
var overflow = board.AddGarbage(2, 4);
|
||||
|
||||
Assert.False(overflow);
|
||||
Assert.True(board[4, Bottom(board)].IsEmpty);
|
||||
Assert.False(board[5, Bottom(board) - 2].IsEmpty);
|
||||
Assert.Equal(19, FilledCount(board));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using ActionBlocks.Game.Core;
|
||||
using ActionBlocks.Game.Items;
|
||||
using ActionBlocks.Game.Targeting;
|
||||
using static ActionBlocks.Game.Tests.TestHelpers;
|
||||
|
||||
namespace ActionBlocks.Game.Tests;
|
||||
|
||||
public class ItemTests
|
||||
{
|
||||
private static ItemContext Context(PlayerGame user, PlayerGame target, double now = 0) =>
|
||||
new(user, target, new Random(3), now);
|
||||
|
||||
[Fact]
|
||||
public void Add_line_inserts_one_garbage_row()
|
||||
{
|
||||
var user = CreatePlayer("a");
|
||||
var target = CreatePlayer("b");
|
||||
|
||||
new AddGarbageLineItem().Apply(Context(user, target));
|
||||
|
||||
Assert.Equal(9, FilledCount(target.Board));
|
||||
Assert.Equal(0, FilledCount(user.Board));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_line_removes_bottom_row()
|
||||
{
|
||||
var target = CreatePlayer("b");
|
||||
FillRow(target.Board, Bottom(target.Board), 0);
|
||||
target.Board[0, Bottom(target.Board) - 1] = new Cell(1);
|
||||
|
||||
new ClearBottomLineItem().Apply(Context(target, target));
|
||||
|
||||
Assert.Equal(1, FilledCount(target.Board));
|
||||
Assert.False(target.Board[0, Bottom(target.Board)].IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wipe_out_clears_the_board()
|
||||
{
|
||||
var target = CreatePlayer("b");
|
||||
FillRow(target.Board, Bottom(target.Board), 0);
|
||||
|
||||
new ClearBoardItem().Apply(Context(target, target));
|
||||
|
||||
Assert.True(target.Board.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Switch_exchanges_boards()
|
||||
{
|
||||
var user = CreatePlayer("a");
|
||||
var target = CreatePlayer("b");
|
||||
FillRow(target.Board, Bottom(target.Board), 0);
|
||||
|
||||
new SwapBoardItem().Apply(Context(user, target));
|
||||
|
||||
Assert.Equal(9, FilledCount(user.Board));
|
||||
Assert.True(target.Board.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Switch_pushes_active_piece_up_when_it_would_overlap()
|
||||
{
|
||||
var user = CreatePlayer("a");
|
||||
var target = CreatePlayer("b");
|
||||
user.Start();
|
||||
for (var y = 20; y < target.Board.Height; y++)
|
||||
{
|
||||
FillRow(target.Board, y, 0);
|
||||
}
|
||||
|
||||
new SwapBoardItem().Apply(Context(user, target));
|
||||
|
||||
Assert.True(user.IsAlive);
|
||||
Assert.True(user.Board.Fits(user.Active!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blackout_hides_board_for_four_seconds()
|
||||
{
|
||||
var target = CreatePlayer("b");
|
||||
target.Start();
|
||||
|
||||
new InvisibilityItem().Apply(Context(target, target, now: 10));
|
||||
|
||||
target.Update(0.01, 13.9);
|
||||
Assert.True(target.IsInvisible);
|
||||
target.Update(0.01, 14.1);
|
||||
Assert.False(target.IsInvisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_contains_five_unique_items()
|
||||
{
|
||||
Assert.Equal(5, ItemRegistry.Default.All.Select(i => i.Id).Distinct().Count());
|
||||
}
|
||||
}
|
||||
|
||||
public class MatchTests
|
||||
{
|
||||
private static Match CreateMatch(params string[] ids) =>
|
||||
new(ids.Select(id => new MatchPlayer(id, id)), new GameSettings { CountdownSeconds = 0 }, seed: 1);
|
||||
|
||||
[Fact]
|
||||
public void Using_an_item_hits_selected_target_and_emits_event()
|
||||
{
|
||||
var match = CreateMatch("a", "b");
|
||||
var a = match.Find("a")!;
|
||||
var b = match.Find("b")!;
|
||||
a.AddItem("add_line");
|
||||
match.SelectTarget("a", "b");
|
||||
|
||||
match.HandleInput("a", InputAction.UseItem);
|
||||
|
||||
Assert.Empty(a.Inventory);
|
||||
Assert.Equal(9, FilledCount(b.Board));
|
||||
var used = Assert.Single(match.DrainEvents().OfType<ItemUsedEvent>());
|
||||
Assert.Equal("b", used.TargetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_player_alive_wins()
|
||||
{
|
||||
var match = CreateMatch("a", "b", "c");
|
||||
|
||||
match.Forfeit("b");
|
||||
Assert.Equal(MatchPhase.Running, match.Phase);
|
||||
match.Forfeit("c");
|
||||
|
||||
Assert.Equal(MatchPhase.Finished, match.Phase);
|
||||
Assert.Equal("a", match.WinnerId);
|
||||
Assert.Equal(3, match.Places["b"]);
|
||||
Assert.Equal(2, match.Places["c"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Target_falls_back_to_self_when_target_is_eliminated()
|
||||
{
|
||||
var match = CreateMatch("a", "b", "c");
|
||||
match.SelectTarget("a", "b");
|
||||
|
||||
match.Forfeit("b");
|
||||
|
||||
Assert.Equal("a", match.Find("a")!.TargetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Garbage_targets_cycle_through_living_opponents()
|
||||
{
|
||||
var match = CreateMatch("a", "b", "c");
|
||||
var targeter = new CyclingGarbageTargeter();
|
||||
var a = match.Find("a")!;
|
||||
|
||||
var targets = Enumerable.Range(0, 4).Select(_ => targeter.NextTarget(a, match.Players)!.PlayerId).ToList();
|
||||
|
||||
Assert.Equal(["b", "c", "b", "c"], targets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Countdown_delays_the_start()
|
||||
{
|
||||
var match = new Match([new MatchPlayer("a", "a")], new GameSettings { CountdownSeconds = 1 });
|
||||
Assert.Equal(MatchPhase.Countdown, match.Phase);
|
||||
Assert.Null(match.Find("a")!.Active);
|
||||
|
||||
match.Update(1.01);
|
||||
|
||||
Assert.Equal(MatchPhase.Running, match.Phase);
|
||||
Assert.NotNull(match.Find("a")!.Active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using ActionBlocks.Game.Core;
|
||||
using static ActionBlocks.Game.Tests.TestHelpers;
|
||||
|
||||
namespace ActionBlocks.Game.Tests;
|
||||
|
||||
public class PlayerGameTests
|
||||
{
|
||||
[Fact]
|
||||
public void Piece_spawns_centered_and_falls_with_gravity()
|
||||
{
|
||||
var player = CreatePlayer(pieces: PieceType.T);
|
||||
player.Start();
|
||||
var startY = player.Active!.Y;
|
||||
Assert.Equal(3, player.Active.X);
|
||||
|
||||
player.Update(1.1, 1.1);
|
||||
|
||||
Assert.Equal(startY + 1, player.Active!.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hard_drop_clears_line_and_collects_item_in_row()
|
||||
{
|
||||
var player = CreatePlayer(pieces: PieceType.I);
|
||||
var bottom = Bottom(player.Board);
|
||||
FillRow(player.Board, bottom, 3, 4, 5, 6);
|
||||
player.Board[0, bottom] = new Cell(CellColor.Garbage, "swap");
|
||||
player.Start();
|
||||
|
||||
player.HandleInput(InputAction.HardDrop);
|
||||
|
||||
Assert.Equal(1, player.Lines);
|
||||
Assert.Equal(["swap"], player.Inventory);
|
||||
Assert.Equal(0, FilledCount(player.Board));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Item_inside_piece_is_collected_when_its_row_clears()
|
||||
{
|
||||
var player = CreatePlayer(itemChance: 1, pieces: PieceType.I);
|
||||
FillRow(player.Board, Bottom(player.Board), 3, 4, 5, 6);
|
||||
player.Start();
|
||||
var itemId = player.Active!.Spec.ItemId;
|
||||
Assert.NotNull(itemId);
|
||||
|
||||
player.HandleInput(InputAction.HardDrop);
|
||||
|
||||
Assert.Equal([itemId!], player.Inventory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Item_stays_on_board_when_row_is_not_cleared()
|
||||
{
|
||||
var player = CreatePlayer(itemChance: 1, pieces: PieceType.I);
|
||||
player.Start();
|
||||
|
||||
player.HandleInput(InputAction.HardDrop);
|
||||
|
||||
Assert.Empty(player.Inventory);
|
||||
var bottom = Bottom(player.Board);
|
||||
Assert.Contains(Enumerable.Range(0, 10), x => player.Board[x, bottom].ItemId is not null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hold_swaps_piece_once_per_lock()
|
||||
{
|
||||
var player = CreatePlayer(pieces: [PieceType.T, PieceType.O, PieceType.S]);
|
||||
player.Start();
|
||||
|
||||
player.HandleInput(InputAction.Hold);
|
||||
Assert.Equal(PieceType.T, player.Hold!.Type);
|
||||
Assert.Equal(PieceType.O, player.Active!.Type);
|
||||
|
||||
player.HandleInput(InputAction.Hold);
|
||||
Assert.Equal(PieceType.O, player.Active!.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pending_garbage_rises_after_a_lock_without_clear()
|
||||
{
|
||||
var player = CreatePlayer(pieces: PieceType.O);
|
||||
player.Start();
|
||||
player.QueueGarbage(2);
|
||||
|
||||
player.HandleInput(InputAction.HardDrop);
|
||||
|
||||
var bottom = Bottom(player.Board);
|
||||
Assert.Equal(9, Enumerable.Range(0, 10).Count(x => player.Board[x, bottom].Color == CellColor.Garbage));
|
||||
Assert.Equal(0, player.PendingGarbage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Double_clear_sends_one_line()
|
||||
{
|
||||
var player = CreatePlayer(pieces: PieceType.O);
|
||||
var bottom = Bottom(player.Board);
|
||||
FillRow(player.Board, bottom, 4, 5);
|
||||
FillRow(player.Board, bottom - 1, 4, 5);
|
||||
player.Board[0, bottom - 2] = new Cell(1); // avoid a perfect clear
|
||||
player.Start();
|
||||
|
||||
player.HandleInput(InputAction.HardDrop);
|
||||
|
||||
Assert.Equal([1], player.DrainOutgoingAttacks());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stacking_to_the_top_ends_the_game()
|
||||
{
|
||||
var player = CreatePlayer(pieces: PieceType.O);
|
||||
player.Start();
|
||||
for (var i = 0; i < 30 && player.IsAlive; i++)
|
||||
{
|
||||
player.HandleInput(InputAction.HardDrop);
|
||||
}
|
||||
Assert.False(player.IsAlive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inventory_is_capped()
|
||||
{
|
||||
var player = CreatePlayer();
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
player.AddItem("nuke");
|
||||
}
|
||||
Assert.Equal(new GameSettings().MaxInventory, player.Inventory.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using ActionBlocks.Game.Core;
|
||||
using ActionBlocks.Game.Garbage;
|
||||
using ActionBlocks.Game.Items;
|
||||
|
||||
namespace ActionBlocks.Game.Tests;
|
||||
|
||||
internal sealed class FixedRandomizer(params PieceType[] sequence) : IRandomizer
|
||||
{
|
||||
private int _index;
|
||||
|
||||
public PieceType Next() => sequence[_index++ % sequence.Length];
|
||||
}
|
||||
|
||||
internal static class TestHelpers
|
||||
{
|
||||
public static PlayerGame CreatePlayer(string id = "p1", double itemChance = 0, params PieceType[] pieces) =>
|
||||
new(
|
||||
id,
|
||||
id,
|
||||
new GameSettings { ItemSpawnChance = itemChance },
|
||||
new FixedRandomizer(pieces.Length == 0 ? [PieceType.I] : pieces),
|
||||
new ItemSpawner(ItemRegistry.Default, itemChance, new Random(1)),
|
||||
new SuperRotationSystem(),
|
||||
new StandardGarbageCalculator(),
|
||||
new StandardScoreCalculator(),
|
||||
new Random(1));
|
||||
|
||||
public static int Bottom(Board board) => board.Height - 1;
|
||||
|
||||
/// <summary>Fills a row except the given columns.</summary>
|
||||
public static void FillRow(Board board, int y, params int[] holes)
|
||||
{
|
||||
for (var x = 0; x < board.Width; x++)
|
||||
{
|
||||
board[x, y] = holes.Contains(x) ? Cell.Empty : new Cell(CellColor.Garbage);
|
||||
}
|
||||
}
|
||||
|
||||
public static int FilledCount(Board board) =>
|
||||
Enumerable.Range(0, board.Height).Sum(y => Enumerable.Range(0, board.Width).Count(x => !board[x, y].IsEmpty));
|
||||
}
|
||||
Reference in New Issue
Block a user