Click here to Skip to main content
16,004,507 members
Articles / Programming Languages / C#
Article

A Blazor Server Fluxor Implementation 2048

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
5 Sep 2024CPOL6 min read 1.3K   21  
The 2048 tile sliding game hosted by Blazor Server and implemented using the Fluxor framework
An introduction to using the BlazorServer web hosting framework and Fluxor middleware to produce interactive web-based applications. An example application of the 2048 tile sliding game is used to illustrate the techniques and patterns employed.

Introduction

This article details a Blazor Server implementation of the 2048 tile sliding game that employs the Fluxor.Blazor.Web Nuget package of Message Oriented Middleware. The emphasis here will be on integrating several Blazor Components into the Fluxor pattern. The game engine, that progresses the game, was described in a previous article.

The Blazor Server Web Hosting Pattern

If you are new to Blazor, it may be useful to know that the Blazor Server Hosting pattern builds web pages by the use of components. Components are, typically, entities containing HTML mark up and code that handle  specific aspects of the user interface that a razor page hosts. Any processing that the application requires is done on the server and the components respond to that activity by updating the user interface (UI). Communication between the server and components is handled by the framework so there's usually no requirement to write bespoke javascript to enable it.

Implementing the Fluxor Pattern

The state of variables relating to a Blazor component does not normally persist after the component goes out of scope so when the component comes back into scope they revert to their default values. The Fluxor pattern persists the state of components throughout the application by providing and managing State classes. Fluxor also enables a component to act solely as a user interface with no knowledge of both a game engine and any external components. A component typically references a State class that holds the UI variables, but the component doesn't directly update the State class. Any actions instigated by a component are dispatched in a message to an Effects class. The Effects class processes any tasks that the action requires. Changes to the state of the component arising from the tasks are then dispatched in a message to a Reducers class. A reducer method takes the pending changes from the message and the existing State and combines them into a new State instance. The new State instance triggers an update response in the component and the component 're-renders’. This may seem like a protracted and pedantic implementation but it gives an excellent degree of separation between Fluxor defined entities. Component communication with the outside world is limited to receiving and sending messages.  but they don't know anything about the sender of messages they receive or the recipient of messages that they dispatch

Some Design Considerations.

Fluxor uses distinct execution pathways known as 'use cases' to progress the application and facilitate debugging. Important design considerations are to determine how many use cases are required and the optimal way to apply the Fluxor pattern of event, effect, state change to each one. The example application is designed to have two use cases.

  1. A DashboardUseCase. This use case manages a DashboardComponent and the processes associated with it to enable the display of variables related to the state of play and to handle opening and closing the game
  2. GameboardUseCase. This is concerned with the implementation of the game engine. The main component here is the GameboardComponent that's used to house the 4 rows of 4 child TileComponents that the game requires

There is another component the NavigationComponent that controls the simulation of sliding the tiles across the horizontal and vertical planes of the game board. It is a member of the GameboardUseCase namespace but it also changes the state of the DashboardUseCase. The suggested file structure to hold the Fluxor related entities is to use a parent directory named Store with child directories named after the use cases. These child directories hold an Effects class a Reducers class and a State class. The message classes are placed in the parent directory

The Razor Page

The Components are hosted on a single Razor page. The app uses Blazor's integrated Bootstrap to layout the main components.

<h1>2048</h1>
<DashboardComponent />
<div class="container">
    <div class="d-sm-flex flex-row justify-content-evenly">
        <GameBoardComponent />
        <NavigationComponent />
    </div>
</div>

The Code initialises the DashboardComponent and GameboardComponent only on the first render as the DashboardResetAction results in the state's IsInitialised variable being set to true. The Dispatcher 'posts' the messages

C#
[Inject]
public IDispatcher? Dispatcher { get; set; }
[Inject]
private IState<DashboardState>? DashboardState { get; set; }
protected override void OnInitialized()
{
   if(DashboardState!.Value.IsInitialised is false)
   {
        Dispatcher!.Dispatch(new DashboardResetAction());
        Dispatcher.Dispatch(new GameBoardResetAction());
   }
    base.OnInitialized();
}

The Messages that the Dispatcher uses are record types. Records are immutable so a new message needs to be constructed every time it is used. The record syntax makes them easy to define.

C#
//A message without any properties
public record DashboardResetAction();
//A message with read only public properties
public record UpdateBoardAction(int NewTileId, bool IsRunning);

The Gameboard Component.

@using BlazorServer2048.Store
@using BlazorServer2048.Components
@inherits Fluxor.Blazor.Web.Components.FluxorComponent


<div class="container">
 @for(int r=0;r<4;r++)
{
  <div class="d-sm-flex flex-row">
  @for(int c=0;c<4;c++)
   {
     <TileComponent Row="@r" Col="@c"/>
   }
  </div>
}
  
</div>

It's important that the Component inherits from FluxorComponent as that is the Fluxor Framework's user interface base class. The Component uses nested for loops to render 4 rows of 4 columns of TileComponents. All the functionality is contained within the child TileComponent

Image 1

The TileComponent.

Scalable Vector Graphics (Svg) are used to display a rectangle and a text type that's centred in the middle of the rectangle. The fill colour and the text are rendered dynamically.

<div>
    <svg width="100" height="100">
        <rect x="0" y="0" width="100" height="100" 
              fill="@colourList[GameboardState!.Value.BoardTiles[Id].TileValue]" 
              stroke="white" stroke-width="8" />
        <text x="50%" y="50%" dominant-baseline="middle" font-weight="bold" 
              text-anchor="middle">@GameboardState!.Value.BoardTiles[Id].FaceValueText</text>
    </svg>
</div>

The Component's backing class, BoardTile, has a TileValue property that's stored as a power of 2 with a range of 0 to 11. It's used to index into a ColourList to return the fill value. Another property, FaceValueText, uses its getter to transform the TileValue into the appropriate string representation of the decimal value of the power. An empty string is returned when the power is 0 as it represents a  blank tile without a value. The simulation of tile movement is achieved by transferring a tile's value to and from adjacent tiles

C#
public string FaceValueText
  {
   get
    {
      int faceValue = TileValue == 0 ? 0 : (1 << TileValue);//2 to the power of TileValue
      return faceValue == 0 ? string.Empty : faceValue.ToString();
    }

  }

The NavigationComponent.

The NavigationComponent is simply 4 Buttons laid out using Bootstrap's row and col classes.

<div class="container" >
    <div class="d-flex flex-column justify-content-center vertical-box" >

        <div class="row" >

            <h4 >Slide Control </h4 >
        </div >
        <div class="row" >

            <div class="col-md-1 offset-md-1" >
                <button type="button" class="btn btn-primary"
               @onclick="((args) = > OnDirectionUpdated(Direction.Up))" >N </button >
            </div >
        </div >
        <div class="row" >
            <div class="col-md-1 " >
                <button type="button" class="btn btn-primary"
               @onclick="((args) = > OnDirectionUpdated(Direction.Left))" >W </button >
            </div >
            <div class="col-md-1 offset-md-1" >
                <button type="button" class="btn btn-primary"
               @onclick="((args) = > OnDirectionUpdated(Direction.Right))" >E </button >
            </div >
        </div >
        <div class="row" >
            <div class="col-md-1 offset-md-1" >
                <button type="button" class="btn btn-primary"
               @onclick="((args) = > OnDirectionUpdated(Direction.Down))" >S </button >
            </div >
        </div >
    </div >
</div >

Image 2

The OnDirectionUpdated method dispatches a DirectionSelectedAction with the appropriate member of the Direction enum as a property value. The handler for the message is inside GameboardUseCase Events class

C#
[EffectMethod]
 public Task HandleDirectionSelectedAction(DirectionSelectedAction action, IDispatcher dispatcher)
 {
 int score;
 if (GameService.IsRunning)
 {
     (bool isRunning, score, int newTileId) = GameService.PlayMove(action.Direction);
     dispatcher.Dispatch(new DirectionSelectedActionResult(isRunning, score + State.Value.Total));
     //if new tile Id is -1 no tile is required
     if (newTileId == -1) return Task.CompletedTask;
     //let the GameboardUseCase Effects class update the board
     dispatcher.Dispatch(new UpdateBoardAction(newTileId,isRunning));
 }
 return Task.CompletedTask;
 }

The DashboardComponent.

Image 3

The DashboardComponent updates when the DashboardState changes. But it also responds to the receipt of a GameOverAction message by displaying a dialog box (Modal) when the game ends. It subscribes to receiving the GameOverAction message by calling the FluxorComponent's SubscribeToAction method and uses the Nuget package Blazored.Modal to deploy the dialog box.

C#
CascadingParameter]
 public IModalService? Modal { get; set; }

 protected override void OnInitialized()
 {
 base.OnInitialized();
 //need to resubscribe every time OnInitialized is called
 SubscribeToAction<GameOverAction>(async (action) => await OnGameOver(action.Message));
 }

 private async Task OnGameOver(string msg)
 {
  var options = new ModalOptions { Position = ModalPosition.TopRight };
  var modalComponent = Modal?.Show<ModalComponent>(msg, options);
  if (modalComponent == null) return;
  ModalResult result = await modalComponent.Result;
  if (result.Confirmed)
   {
     StopStartSelector();
   }
 }
 private void StopStartSelector()
 {
  Dispatcher!.Dispatch(new StopStartAction());
 }

Image 4

The State Classes

The DashboardState holds several value types. State classes need to have a default constructor.

C#
[FeatureState]

public record DashboardState(int Total,
                             bool IsRunning,
                             bool IsGameWon,
                             bool IsInitialised)
{
    public DashboardState() : this(0, false, false,false)
    {

    }
}

The GameboardState needs to store 16 instances of the GameboardComponent's backing class BoardTile. The documentation advises employing an ImmutableArray<T> for that.

C#
 [FeatureState]
public record GameboardState
{
  public GameboardState()
  {
    var boardTiles = new List<BoardTile>(16);

    for (int i = 0; i < 16; i++)
     {
       boardTiles.Add(new BoardTile(i, 0));

     }

    BoardTiles = ImmutableArray.Create(boardTiles.ToArray());
  }

    public ImmutableArray<BoardTile> BoardTiles { get; init; }
}

Reducers Class

The DashboardComponent Reducers Class makes use of the with keyword to update the state.

C#
[ReducerMethod]
  public static DashboardState HandleDirectionSelectedActionResult(
          DashboardState state,
          DirectionSelectedActionResult action)
  {

   return state with { IsRunning = action.IsRunning, Total=action.Total};

  }

The GameboardComponent Reducers class is a bit more complex as it needs to update an immutable array.

C#
[ReducerMethod]
public static GameboardState ReduceUpdateAllTilesActionResult(
              GameboardState state,
              UpdateAllTilesActionResult action)
{
 var updatedTiles = state!.BoardTiles.ToArray();
 foreach (var boardTile in state.BoardTiles)
  {
   if (action.ChangedTileValues!.TryGetValue(boardTile.Id, out int value))
   {
    updatedTiles[boardTile.Id] = new BoardTile(boardTile.Id, value);
   }

  }

 var newArray = ImmutableArray.Create(updatedTiles);
 return state with { BoardTiles = newArray };
}

There is a method that updates a single item an immutable array. It's used when a new tile is inserted after a move is completed

C#
  var updatedTiles= state!.BoardTiles.SetItem(
    action.TileId, new BoardTile(action.TileId, action.TileValue));
return state with { BoardTiles = updatedTiles };

App.razor.

Blazor employs a structured render tree of components to update a page. App.razor is the root component of the tree; by default it has a single child component named Router. Two further components need to be added as shown below.

<Fluxor.Blazor.Web.StoreInitializer/> 
<CascadingBlazoredModal>
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>
</CascadingBlazoredModal>

The CascadingBlazoredModal component is required to enable the modal pop up. The reference to the Fluxor.Blazor.Web.StoreInitializer is needed to setup the Fluxor framework. The framework relies extensively on the appropriate attributes being in place within the Effects, Reducers and State classes. It's the attributes that determine the use of a class rather than the class names but sticking with the names used in the documentation is probably the best option. Other services that the application requires are not included here, they are added within Program.cs

Program.cs.

C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddFluxor(options => options.ScanAssemblies(typeof(Program).Assembly));
builder.Services.AddSingleton<WeatherForecastService>();
//Add the GameService that drives the GameEngine 
builder.Services.AddSingleton<IGameService, GameService>();
builder.Services.AddBlazoredModal();
var app = builder.Build();

Conclusion.

The Fluxor pattern of event, effect, state change when applied to predefined execution pathways can produce robust applications that are easy to maintain, debug and expand.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Student
Wales Wales
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --