当前位置:网站首页>Architectural concept exploration: Taking the development of card games as an example

Architectural concept exploration: Taking the development of card games as an example

2022-06-11 14:37:00 Deep learning and python

author | Enrico Piccinin

translator | Knowingly mountain

planning | Ding Xiaoyun

COVID-19 made me miss meeting my friends 、 Opportunities to discuss and play card games .

Zoom It can solve some urgent problems , But how to play card games ? How to play our Scopone Well ?

therefore , I decided to develop one that I could play with my friends Scopone game , At the same time, test some architecture concepts in the code that I have been fascinated with for a long time .

All the source code of the game can be found in this code base .

1 What answers do I want

Free deployment server

An interactive card game supporting multiple players is composed of client and server . The server is deployed in the cloud , But where on the end ? As a component running on a dedicated server ? Or as Kubernetes Hosting... In a cluster Docker Mirror image ? Or as a serverless function ?

I don't know which is the best choice , But what I care about is whether the maintenance of the core logic of the game can be independent of the deployment model .

Independent of UI Framework or library

“Angular It's the best ”.“ No ,React Better and faster .” Such arguments are everywhere . But does it really matter ? Shouldn't we treat most of the front-end logic as pure Javascript or Typescript Code , Completely independent of UI Frame or library ? I think it's OK , But I still want to have a real try .

Automatically test the possibility of multi-user interaction scenarios

Card games are like other interactive applications today , There are multiple users interacting in real time through the central server . for example , When a player plays a card , Everyone else needs to see this card in real time . In limine , I don't know how to test such applications . Is it possible to use simple JavaScript Test library ( Such as Mocha) And standard test practices automatically test it ?

Scopone The game can answer my questions

Scopone The game provides me with a good chance , Let me answer my own questions in a specific way . therefore , I decided to try to achieve it , See what I can learn from it .

2 The whole idea

Scopone The rules of the game

Scopone It is a traditional Italian card game ,4 Players are divided into 2 Group , Each group 2 people , Yes 40 card .

At the beginning of the game , Every player gets 10 card , The first player plays the first card , This card is placed face up on the table . Then the second player plays cards . If this card has the same level as the card on the table , The second player is from the table “ take ” This card . If there are no cards on the table , The player who takes the card gets “Scopa” Score of . Then the third player plays cards , And so on , Until all the cards are played .

The rules are over , The key point here is , When players play cards , They change the state of the game , for example “ Which papers are facing up ” or “ Which players can play the next card ”.

Application structure and technology stack

Scopone The game requires one server instance and four client instances , Four players start the client on their device .

If we pay attention to the interaction between various elements in the game , We can know :

  • Players perform actions , For example, players play cards ;
  • As a result of the player's actions , All players need to update the status of the game .

This means that the client and server need a two-way communication protocol , Because the client must send commands to the server , The server needs to push the updated status to the client .WebSocket Is a protocol suitable for use here , It is supported by various programming languages .

The server side uses Go The realization of language , Because it's good for WebSocket Have good support , Different deployment models are also supported , let me put it another way , It can be deployed as a dedicated server 、Docker Mirror or Lambda.

The client is a browser based application , In two different ways : One is Angular, The other is React. Both versions use TypeScript and RxJs, To achieve responsive design .

The following figure shows the overall architecture of the game .

Orders and events

In short , The process of this game is like this :

  • The client sends commands to the server through messages ;
  • The server updates the game status ;
  • The server pushes the latest status of the game to the client through a message ;
  • When the client receives a message from the server , Think of it as an event that triggers a client state update .

This cycle repeats itself , Until the end of the game .

3 Free deployment server side

The server receives the command message sent by the client , And update the status of the game according to these commands , Then send the updated status to the client .

Client pass WebSocket Channel sends command messages , It will be converted to server specific API Call to .

API The call generates a response , It will be converted into a set of messages , The news passed through WebSocket The channel is sent to each client .

therefore , There are two different layers on the server side , They have different responsibilities : Game logic layer and WebSocket Mechanism layer .

Game logic layer

This layer is responsible for implementing game logic , That is, update the game status according to the received command , And return to the latest status , Send to each client .

therefore , This layer can use internal states and a set of... That implement command logic API To achieve .API The latest status will be returned to the client .

WebSocket Mechanism layer

This layer is responsible for transferring data from WebSocket The message received by the channel is converted to the corresponding API call . Besides , It also needs to update the status ( call API Generated response ) Converted to messages pushed to the corresponding client .

Dependencies between layers

Based on the previous discussion , The game logic layer is independent of WebSocket, It's just a group of returning states API.

WebSocket The mechanism layer implements WebSocket characteristic , This layer will depend on the selected deployment model .

for example , If we decide to deploy the server side as a dedicated server , Then you need to choose to implement WebSocket Package of agreement ( Here we choose Gorilla), And if we decide to be AWS Lambda Function to deploy , Then we need to rely on WebSocket Agreed Lambda Realization .

If we want to keep the game logic layer and WebSocket The mechanism layer is strictly separated , Is to import the former from the latter ( One way ), Then the game logic layer will worry about which specific deployment model to choose .

Based on this strategy , We can only develop a single version of the game logic , And freely deploy servers everywhere .

This has several advantages . for example , When developing the client , We can run it locally Gorilla WebSocket Realization , This will be very convenient , Even in VSCode Enable debug mode in . This allows you to set breakpoints in the server code , Debug game logic through various commands sent by the client .

When deploying a game to a server in a production environment ( So I can play real-time games with my friends ), You can directly deploy the same game logic to the cloud , For example, Google application engine (GAE).

Besides , When I found out whether we were playing games or not , Google will charge the lowest fee (GAE Always keep at least one server open ), I can migrate the server to... Without changing the game logic code AWS Lambda Of “ On demand ” Charging model .

4 Independent of UI Framework or library

The big problem now is : choice Angular still React?

I also asked myself another question : Is it possible to use TypeScipt Develop most of the client logic , Independent of the front-end framework or library used to manage views ?

it turns out to be the case that , At least in this case , It is possible , There are just some interesting side effects .

Design of application front end : View layer and service layer

There are three simple ideas for designing the front end of an application :

  • The client is divided into two layers :
  • The view layer is a composable component (Angular and React You can put UI As a combination of components ), Pure presentation logic can be implemented .
  • Service layer , use TypeScript Realization , Not any Angular or React State management , Handle the commands calling the remote server and interpret the state change response from the server .
  • The service layer provides two types of services for the view layer API:
  • Public methods —— Call these methods to invoke commands on the remote server , Or change the state of the client .
  • Public event flow —— Implemented as a RxJs Observable, It can be used by anyone who wants to be notified of a status change UI Component subscription .
  • The view layer has only two simple responsibilities :
  • Intercept UI Event and transform it into a public to the service layer API Method call .
  • Subscribe to public API Observable, And make corresponding representation changes to the received notice .

A view - service - Server interaction example

Players can play a card by clicking on the face

A little bit more specific , Let's see how to play a card .

We assume that Player_X Will play the next card .Player_X Click on “ Heart A” Card face , This UI Events will trigger “Player_X Hit the heart A” This action .

Here are the steps the application will go through :

The view layer intercepts user generated events , And call the service layer playCard Method , Parameter is “ Heart A”.

The service layer sends messages to the remote server “Player_X Hit the heart A”.

The remote server updates the status of the game , And notify all clients that the state has changed . for example , It tells all clients Player_X Which card was played and who was the next player to play .

The service layer of each client receives the status update message sent by the remote server , And pass Observable Stream into notification of specific events . for example ,Player_X Received by the client service layer of isMyTurnToPlay by false, because Player_X Definitely not the next player . If the other player is Player_Y, Player_Y Received by the service layer of the client isMyTurnToPlay It will be true.

Each client's view layer subscribes to the event stream published by the service layer , And react to the event notification , Update on demand UI. for example ,Player_Y( The next player ) The view layer of allows the client to play a card , Other players' clients will not have this action .

The interaction between view layer and service layer

Light components and heavy services

Based on these rules , We finally built “ Light components ”, It only manages UI concerns ( Represents and UI Event handling ), and “ Heavy service ” Is responsible for handling all logic .

most important of all ,“ Heavy service ”( Contains most of the logic ) Completely independent of what is used UI Framework or library . It does not rely on Angular It doesn't depend on React.

of UI More details about layers can be found in the appendix section of this article .

The benefits of doing this

What are the benefits of doing so ?

Of course not the portability between different frameworks and libraries . Once you choose Angular, It is unlikely that anyone will want to switch to React, vice versa , But there are still some advantages .

One advantage of this method is , If implemented thoroughly , It will standardize the way we develop the front end , And easier to understand . In the final analysis , This is only through customization ( The service layer is customized ) Design one-way information flow . Customization has a low level of abstraction , It's simpler , But it may take some effort “ Reinvent the wheel ” The price of .

however , The biggest benefit is that the application has better and easier testability .

UI Testing is very complicated , No matter which framework or library you use .

But if we convert most of the code to pure TypeScript Realization , Testing will become easier . We can use the standard test framework to test the core logic of the application ( Here we use Mocha), We can also handle complex test scenarios in a relatively simple way , We will discuss in the next section .

5 Automatically test real-time multi-user interaction scenarios

Scopone It's a four person game .

4 Clients must pass WebSocket Connect to a central server . An operation performed by a client , for example “ Play a card ”, Will trigger all client updates ( That is, the so-called side effects ).

This is a real-time multi-user Interaction scenario . This means that if we want to test the behavior of the entire application , You need to run multiple clients and a server at the same time .

How do we test these scenarios automatically ? We can use standard JavaScript Test libraries to test them ? Can we test them on an independent developer workstation ? These are the questions to be answered next . The fact proved that , All these things are possible , At least to a large extent it is possible .

What is the real-time multi-user Interaction scenario testing

Let's take a simple example , Suppose we want to test that the card distribution of all players at the beginning of the game is correct . After the new game starts , All clients receive... From the server 10 card (Scopone The game has 40 card , Each player can get 10 Zhang ).

If we want to be in a separate machine ( such as , Developer's machine ) Automatically test this behavior on , You need a local server . We can do that , Because the server can be used as a local container or WebSocket Server running . therefore , We assume that there is a local server running on our machine .

however , To run tests , We also need to find a way to create the right context and actions that can trigger the side effects we want to test ( The distribution of cards is a side effect of a player starting the game ). let me put it another way , We need to find a way to simulate the following :

  • 4 Players launch applications and join the same game ( Create the right context );
  • A player starts the game ( Trigger the side effects we want to test ).

Only in this way can we check whether the server will issue the expected cards to all players .

A test case for a multi-user scenario

6 How to simulate multiple clients

Each client consists of a view layer and a service layer .

Service layer API( Methods and Observable flow ) Is defined in a class (ScoponeServerService class ).

Each client creates an instance of this class , And connect to the server . The view layer interacts with its service class instances .

If we want to simulate 4 A client , create 4 Different instances , And connect them all to our local server .

establish 4 Service class instances , representative 4 Different clients

How to create a context for a test

Now? , We have it. 4 Clients already connected to the server , We need to build the right context for the test . We need to 4 Players , And wait for them to join the game .

Create context for the test

Last , How to perform tests

In the creation of 4 After two clients and the correct context , Then we can run the test . We can have a player send a command to start the game , Then check whether each player has received the expected number of cards .

Run the test

Close together

The test of multi-user Interaction scenario is as follows :

  • Create a service instance for each user ;
  • Send commands to the service in the correct order , Create test context ;
  • Send the command to trigger the side effect ( Is the command being tested );
  • Verify the of each service Observable API Notice given , That is, the result of the command ( side effect ), Whether the expected data is included .

This is the service layer API Of BDD

We can think of this approach as targeting the service layer API Behavior driven development (BDD) test .

according to BDD The specification of , The test behavior is like this :

  • Assume the initial scenario :4 Players join the game ;
  • Time : Players start playing ;
  • then : We want every player to get 10 card .

The test function uses a DSL Compiling , It consists of some special auxiliary functions , The combination of these functions creates the context (playersJoinTheGame Is an example of an auxiliary function ).

It is not an end-to-end test , But it can be very powerful

This is not a complete end-to-end test . We did not test the view layer .

But it can still be a very powerful tool , Especially if we insist “ Light components and heavy services ” The rules of .

If the view layer consists of light components , And most of the logic is concentrated in the service layer , Then we can cover the core of the application behavior , Whether it is client-side or server-side , We only need to make relatively simple settings , Use standard tools ( We used Mocha Test library , It's definitely not the latest and brightest frame ), And on the developer's machine .

The advantage of this is , Developers can write test suites that can be executed quickly , Increase the frequency of test execution . meanwhile , Such a test suite actually tests the entire application logic from the client to the server ( Even multi-user real-time applications ), Provides a high level of credibility .

7 Conclusion

Developing card games is an interesting experience .

Besides bringing me some fun during the outbreak , It also gives me the opportunity to explore architectural concepts through code .

We often use architectural concepts to express our views . I find , Put these concepts into practice , Even simple proof of concept , It will also increase our understanding of them , Let's have more confidence in using them in real projects .

8 appendix : View layer mechanism

The components in the view layer mainly do two things :

  • Handle UI Events and convert them to service commands .
  • Subscribe to the stream exposed by the service , And update UI To respond to events .

In order to explain the meaning of the last point more specifically , Let's give you an example : How to determine who is the next player to play .

As we said , One of the rules of the game is that players can play one card after another . for example , If Player_X Is the first player ,Player_Y Is the second player , So in Player_X After playing a card , Only Player_Y To play the next card , No other player can play cards . This information is part of the state maintained by the server .

Each time a card is played , The server will send a message to all clients , Specify who the next player is .

The service layer passes through a process called enablePlay Of Observable The flow converts messages into notifications . If the message says that the player can play the next card , The service layer through enablePlay The value of the notification is true, Otherwise, it would be false.

Components that allow players to play cards must be subscribed enablePlay$ flow , And respond accordingly to the notified data .

In our React In the implementation , This is a called Hand Functional components of . This component defines a state variable enablePlay, Its value represents the probability of playing cards .Hand Component subscribes to enablePlay Observable flow , Whenever it receives enablePlay At the notice of , By setting enablePlay To trigger UI Repaint .

Here's how to use React Of Hand Component to implement this specific function .

export const Hand: FC = () => {
 const server = useContext(ServerContext);
  . . .
 const [handReactState, setHandReactState] = useState<HandReactState>({
   . . .
   enablePlay: false,
 });
  . . .
 useEffect(() => {
  . . .
  . . .
   const enablePlay$ = server.enablePlay$.pipe(
     tap((enablePlay) => {
       setHandReactState((prevState) => ({ ...prevState, enablePlay }));
     })
   );


   const subscription = merge(
       . . .
     handClosed$
   ).subscribe();


   return () => {
     console.log("Unsubscribe Hand subscription");
     subscription.unsubscribe();
   };
 }, [server]);
  . . .


 return (
   <>
       . . .
       <Cards
           . . .
         enabled={handReactState.enablePlay}
       ></Cards>  
       . . .
   </>
 );
};

Angular Version logic is the same , And in HandComponent Implemented in . The only difference is that enablePlay$ Observable The subscription of the stream is directly through the template async Pipeline completed .

Author's brief introduction :

Enrico Piccinin For code and IT Interested in the odd things that happen occasionally in the organization . By virtue of IT Years of experience in the development field , He wished to know that “ new IT” What happens in a traditional organization . You can enricopiccinin.com or LinkedIn Found on the Enrico.

Link to the original text :

https://www.infoq.com/articles/exploring-architecture-building-game/

原网站

版权声明
本文为[Deep learning and python]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/162/202206111418149373.html