Amuse Tutorial

Author: Giovanni Caire (TILab)
Date: 23/12/2015

1. Introduction

This Tutorial aims at showing how to exploit the AMUSE (Agent-based Multi User Social Entertainment) framework to build multi-player games i.e. games involving two or more players competing or cooperating within the scope of a match (or more in general of a virtual context).

More information on the AMUSE framework as well as a detailed explanation on how to setup the AMUSE development environment can be found in the AMUSE Startup Guide.

Prerequisites:

  • AMUSE is fully written in Java –> a good knowledge of the Java language and the Java API is mandatory to understand the content of this tutorial
  • In this initial version AMUSE client API are available for the Android environment only –> The reader is assumed to be already familiar with Android programming
  • The AMUSE framework is based on JADE (Java Agents DEvelopment framework) and WADE (Workflow and Agents Development Environment) –> Being familiar with JADE and WADE is not necessary to use the AMUSE client API, but is helpful, to develop game specific logics to be executed server side for games that require it.

1.1 The Multi-player Tris game

In order to explain the main AMUSE features, this Tutorial makes reference to pieces of code taken from the Multi-player Tris game (shortly MTris) whose complete source code is available for download from the AMUSE SVN repository at

The client App Apk ready to be installed on an Android terminal, can be downloaded following this link. Zip files containing the server-part and the client-part sources can be downloaded here

NOTE: Some screenshots reported in this tutorial may appear slightly different with respect to those of  the Multi-player Tris App depending on the terminal and the actual App version you are using.

The MTris game is an extension of  the well known Tic-Tac-Toe game involving however 3 players playing on a 6 x 7 grid as shown in the picture below. Each player sees her marked spaces (red circles), but is not able to distinguish between spaces marked by other players (black crosses). Whenever a player marks three consecutive spaces, i.e. a “tris” (both in horizontal, vertical and diagonal) he scores 1 point. When the grid is full the player with the highest score wins.

MTris_screenshot2

The Multi-player Tris Client App main screen

Besides developing the code to show the grid, to make the right sign appears when a user marks a space, to compute the score of each player and to decide who the winner is at the end of the match (all this stuff is specific of the MTris game application), there are a number of common issues to be taken into account such as

  • Match organization: how to coordinate three players so that they can start playing on the same grid?
  • Match playing: how to ensure that one player can move (mark a space) only when his/her turn comes? How to make all players aware about the space marked by the moving player?
  • General: managing player identity and associated information (e.g. best score, avatar…), managing friends, how to know whether a given player is currently online or not…

The AMUSE framework focuses on these aspects by means of

  1. An on-line platform running AMUSE common logics as well as any game specific server-side logics (following a Platform-as-a-Service approach) for games that require it.
  2. A set of client API for the Android environment to be used to implement the above mentioned aspects inside the game App
  3. A set of server API to implement, when needed, the game-specific centralized match coordination logics that will run in the AMUSE online platform.

The AMUSE framework instead does NOT address graphics related aspects such as additional nice themes, support for animations, graphic engines …

This tutorial is organized as follows:

  • Section 2 presents the structure of the MTris client Android App.
  • Section 3 gives a brief overview of the AMUSE client API and then focuses on the aspects related to connecting and authenticating to the AMUSE platform.
  • Section 4 shows how the MTris Android App uses the AMUSE core features to manage user’s information.
  • Section 5 shows how the MTris Android App uses the Games Room feature to support multi-player match organization and playing.
  • Section 6 gives a brief overview of the AMUSE server API and focuses on the server part of the MTris application.

 

2. MTris client Android App structure

The MTris client Android App, as usual for Android Apps, is composed of a set of Activity classes implementing the relevant screens, an Application class (com.amuse.mtris.MTris) used to provide easy access to AMUSE stuff (as will be described in next sections) to all activities and to collect utility methods and a few additional classes. The Activity classes are briefly described below.

StartupActivity. This is the activity that is launched at application startup and is responsible for activating the connection to the AMUSE platform and to allow the user inserting username and password if necessary. The code of the StartupActivity class is described in section 3.
WelcomeActivity. This is the activity that is shown as soon as the connection to the AMUSE platform is established. It shows buttons by means of which the user can start a match (MainActivity), view/manage his/her properties (UserInfoActivity), manage friends and check their presence status (FriendsActivity) and see a small description of the AMUSE initiative (AboutAmuseActivity).

All classes are included in the com.amuse.mtris package.

The Manifest file of the MTris client App is shown below.

Such Manifest file should be quite easy to understand for people familiar with Android application development. Two points have to be highlighted however:

  • The permissions to be declared (lines 10 and 11)
  • The services to be used (lines 18 and 19)

These are common to all Amuse based Android Apps.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.amuse"
      android:versionCode="1"
      android:versionName="0.8">
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="19" />

	<uses-permission android:name="android.permission.INTERNET" />	
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
		
    <application 
    	android:name=".mtris.MTris"
    	android:icon="@drawable/mtris_apk_icon"
    	android:label="@string/app_name">
    	
        <service android:name="com.amuse.client.android.AmuseService" />	
        <service android:name="jade.android.MicroRuntimeService" />	
		
        <activity android:name=".mtris.StartupActivity"
                  android:screenOrientation="portrait"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".mtris.WelcomeActivity"
                  android:screenOrientation="portrait"
                  android:label="@string/welcome">
        </activity>
        
        <activity android:name=".mtris.MainActivity"
                  android:screenOrientation="portrait"
                  android:label="@string/play">
        </activity>        
        
        <activity android:name=".mtris.FriendsActivity"
                  android:screenOrientation="portrait"
                  android:label="@string/friends"
                  android:windowSoftInputMode="stateHidden">
        </activity>        
        
        <activity android:name=".mtris.UserInfoActivity"
                  android:screenOrientation="portrait"
                  android:label="@string/user_info">
        </activity>        
        
        <activity android:name=".mtris.AboutAmuseActivity"
                  android:screenOrientation="portrait"
                  android:label="@string/about_amuse">
        </activity>        

    </application>
</manifest>

3. Connecting to the AMUSE platform

In this section we provide a brief overview of the AMUSE client API and then we focus on the aspects related to connecting and authenticating with the AMUSE platform. It has to be noticed, in facts, that regardless of the fact that the game we are developing requires game-specific logics running server-side, all AMUSE features require the client App to be connected to the AMUSE platform.

AMUSE client API are included in the com.amuse.client package and its sub-packages.  The AmuseClient class included in the com.amuse.client.android package in particular is the entry point to access all AMUSE client features. First of all the getInstance() static method must be invoked to retrieve a properly initialized AmuseClient instance. Successively the connect() method allows connecting and authenticating to the AMUSE platform. Once this has been done the getFeature() method allows retrieving the classes actually implementing the AMUSE features. In particular in this tutorial we will describe the UserManagementFeature class to manage basic user’s information, the ContactsManagementFeature class to manage user’s contacts and to be notified about their presence status (online/offline) and the GamesRoomFeature class to organize and coordinate matches according to the table metaphor: players sit at a table and when a sufficient number of players have sat the match starts.

Other relevant feature classes are available, but are not presented in this tutorial since the MTris application does not use them. Refer to the AMUSE Javadoc for a complete list of the AMUSE features and for a description of how to use them.

The following code snippet shows the onCreate() method of the StartupActivity class.

	private DefaultLoginManager loginManager;
	private CallbackManager callbackManager;
	private volatile boolean destroyed;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		Log.i(MTris.TAG, "Starting...");
		super.onCreate(savedInstanceState);
		setContentView(R.layout.startup);
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
			getActionBar().hide();
		}
		destroyed = false;
		callbackManager = new CallbackManager(this);

		// Initialize the AvatarManager
		AvatarManager.initialize(R.drawable.avatar_default, null, -1, -1);

		// If user's credentials are not yet available we have to make the loginForm become visible
		// and hide the progressBar. The DefaultLoginManager utility class does that for us
		loginManager = new DefaultLoginManager(this);
		loginManager.setVisibleLoginFields(new int[]{R.id.loginForm});
		loginManager.setHiddenLoginFields(new int[]{R.id.startup_progressbar});

		// Connect to the Amuse server
		((MTris) getApplication()).connect(loginManager, callbackManager.wrap(Void.class, new Callback<Void>() {
			@Override
			public void onSuccess(Void v) {
				Log.i(MTris.TAG, "Connect SUCCESS !!!!!!");

				// Check if application has been closed
				if( destroyed){return;}

				// Connection to the Amuse server OK --> Initialize the GamesRoomFeature
				// to be prepared to receive invitations.
				((MTris) getApplication()).initGamesRoomFeature();
				// Then start the WelcomeActivity in the Android UI thread and terminate.
				Intent i = new Intent(StartupActivity.this, WelcomeActivity.class);
				startActivity(i);
				StartupActivity.this.finish();
			}

			@Override
			public void onFailure(final Throwable th) {
				Log.w(MTris.TAG, "Connection error", th);
				// Connection to the Amuse server KO --> Show an error message.
				showError(th);
			}
		}));

		// Disable screen rotation
		this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
	}

At line 17 we initialize the AvatarManager. This is a utility class of the AMUSE Android library that facilitates the management of user’s avatars. It is not discussed in this tutorial (refer to the Javadoc for a description of how to use it).
At lines from 21 to 23 we create a DefaultLoginManager and we configure it to show a proper form (the loginForm defined in the startup.xml layout and set invisible by default) to let the user insert his/her username and password in case they are not yet known to the application. The AmuseClient indeed automatically stores the user credentials after the first successful login so that the user is not required to insert them again at successive times.

NOTE: The AMUSE framework allows users to authenticate by means of AMUSE specific credentials as well as Facebook or Google+ credentials. How to authenticate by means of Facebook or Google+ credentials is not addressed in this tutorial however.

At line 26 we connect to the AMUSE platform. If the user’s credentials are not available yet, the DefaultLoginManager makes the loginForm become visible and “blocks” the connection process until the user inserted them.
All AMUSE API that require interactions with the AMUSE platform work asynchronously. As a consequence they always get a Callback argument whose onSuccess() method is invoked when the operation successfully completes. In that method we initialize the GamesRoomFeature that will be used to coordinate match organization and playing (this will be discussed in next sections) and then we start the WelcomeActivity.

Note that Callback methods are typically executed within Amuse internal threads. The CallbackManager utility class of the AMUSE library creates a sort of “wrapper” around a Callback object thus ensuring that Callback methods are executed in the Android UI Thread.

The connect() method of the MTris application class is shown below.

public void connect(final LoginManager lm, final Callback<Void> callback) {
	Log.i(TAG, "Connecting...");
	// Retrieve the AmuseClient
	AmuseClient.getInstance(getResources().getString(R.string.app_name),
	                        getApplicationContext(),
	                        new Callback<AmuseClient>() {

		@Override
		public void onSuccess(AmuseClient c) {
			// AmuseClient successfully retrieved. Store it in a local variable
			amuseClient = c;
			Log.i(TAG, "Amuse client successfully retrieved");

			// Connect to the Amuse server
			Properties pp= new Properties();
			String ip = getResources().getString(R.string.amuse_host);
			pp.setProperty(MicroRuntime.HOST_KEY, ip);
			String port = getResources().getString(R.string.amuse_port);
			pp.setProperty(MicroRuntime.PORT_KEY,  port);
			String proto = getResources().getString(R.string.amuse_proto);
			pp.setProperty(MicroRuntime.PROTO_KEY,  proto);
			amuseClient.setConnectionProperties(pp);
			amuseClient.setLoginManager(lm);
			amuseClient.connect(callback);
		}

		@Override
		public void onFailure(Throwable t) {
			Log.e(TAG, "Error retrieving AmuseClient", t);
		}
	});
}

At line 4 we retrieve a properly initialized AmuseClient instance. This operation is carried out asynchronously and therefore the retrieved AmuseClient is made available in the onSuccess() callback method.
After that, the host, port and protocol properties are specified (lines from 15 to 22) to let the AmuseClient know where the AMUSE online platform is and how to connect to it.
Finally the DefaultLoginManager discussed before is set (line 23) and the actual connection is done (line 24). When the connection will be established and the user authenticated, the onSuccess() method of the callback parameter will be called.

To conclude this section, the following code snippet shows the method of the StartupActivity class that is invoked when the loginForm is shown and the user clicks on the login_button button.

public void login(View v){
	EditText usernameTxt = (EditText) findViewById(R.id.usernameTx);
	EditText passwordTxt = (EditText) findViewById(R.id.passwordTx);
	String username = usernameTxt.getText().toString();
	String password = passwordTxt.getText().toString();
	// This is an existing user --> newUser parameter = false
	loginManager.loginDone(new LoginInfo(username, password, false));
}

Username and password are got from the proper EditText instances (lines 2 to 5) and then the loginDone() method is invoked (line 7) to pass user’s credentials to the DefaultLoginManager and make the connection process go on. The third parameter of the LoginInfo object passed to the loginDone() method is a boolean indicating whether this is a new user that must be registered in the AMUSE platform (true) or an existing user (i.e. a user that already has an AMUSE account) that is logging in (false). A similar method called register() is executed when the user  clicks on the register_button button. The only difference in that method is that it sets the third parameter of the LoginInfo constructor to true.

4. Managing User information

AMUSE features are divided in Core features and Gaming features. Unlike Gaming features, Core features are not specifically oriented to games development and can be used to create generic multi-user applications. As a consequence in such features the term user is used instead of player.

In this section we show how to exploit the UserManagementFeature class to manage local user and remote users information. Such information are treated by the AMUSE framework as a set of key-value properties. The client App of a given user is allowed to set values of properties of the local user only, but can read other users properties. Each property can be defined as public or private. When retrieving information about another user of course only public properties are loaded.

Before getting and setting properties of the local user, the client App must be synchronized with the AMUSE platform. The following code snippet, taken from the onCreate() method of the WelcomeActivity, shows how to do that.

// Synchronize local user information
UserManagementFeature umf = client.getFeature(UserManagementFeature.class);
umf.synchLocalUserInfo(new Callback<Void>() {
	@Override
	public void onSuccess(Void arg0) {
		Log.i(MTris.TAG, "WelcomeActivity: Local User information synchronized.");
	}

	@Override
	public void onFailure(Throwable t) {
		Log.w(MTris.TAG, "WelcomeActivity: Cannot synchronize local user information", t);
	}
});

At line 2 the UserManagementFeature instance is retrieved by means of the getFeature() method of the AmuseClient. Then the synchronization process is activated by means of the synchLocalUserInfo() method. As usual the operation is carried out asynchronously and its completion is notified through the onSuccess() method of the Callback object passed as parameter.

Furthermore the notion of big-property is supported. This is useful to keep potentially big information about a user (e.g a picture  or some application specific structured data). While normal properties are loaded all together when the information of a given user are requested (getUserInfo() method), big-properties must be loaded one by one by means of the loadBigPropertyValue() method. Big-property values are treated as byte arrays (byte[]); it is the responsibility of the application to marshal and un-marshal real big-property values to/from that form.
The MTris client App does not use big-properties directly. More details on how to set and get big-property values can be found in the Javadoc of the UserManagementFeature class.

5. Match organization and playing

In this section we describe how the MTris client App exploits the GamesRoomFeature to support match organization and playing.

5.1. Feature initialization

AMUSE  features typically require some initialization steps before they can be exploited. The GamesRoomFeature in particular requires all game specific structured data types to be exchanged by client Apps to be registered (primitive types such as String, int and boolean do not require any registration). Registered data types must have the form of Java beans i.e. classes with getter and setter methods for all their attributes. The code snippet below (taken from the initGamesRoomFeature() method of the MTris class) shows how this is done in the MTris client App.

// Initialize the GamesRoomFeature
grf = amuseClient.getFeature(GamesRoomFeature.class);
// Register application specific data types
try {
	grf.registerDataType(TurnPlayed.class);
	grf.registerDataType(Point.class);
}
catch (OntologyException oe) {
	// Should never happen
	Log.e(TAG, "Error registering data type", oe);
}
// Set the listener to be notified about incoming JoinTable proposals.
// Use an adapter that translates listener method calls into broadcasted intents
// or notifications in the Android notification bar
NotificationModeInfo nmi = new NotificationModeInfo();
nmi.setNotificationIconId(R.drawable.mtris_apk_icon);
nmi.setNotificationTitle(getResources().getString(R.string.app_name));
nmi.setNotificationText("Invitation");
// When the user clicks in the notification forward him to the WelcomeActivity. The latter will manage the event
nmi.setNotificationActivityClass(WelcomeActivity.class);
GamesRoomEventAdapter adapter = new GamesRoomEventAdapter();
adapter.initialize(amuseClient, nmi);
grf.setListener(adapter);

In the MTris application we define the TurnPlayed class to describe the information to be disclosed when a player makes a move when it’s his/her turn and the Point class to identify a point in the MTris grid. It should be noticed that such classes are not included in the MTris client App sources. Indeed in the MTris application we decided to define classes to be used both client and server side (like TurnPlayed and Point) in the server part project. The MTris.jar file containing shared classes is included among the MTris client App libraries to make such classes visible.

5.2. Handling events

In the second part of the code snippet presented in previous section, we register a listener to be notified about GamesRoomFeature general events. The GamesRoomFeature  issues an event whenever an incoming invitation to join a table and play a match is received. GamesRoomEventAdapter is a ready made utility class of the AMUSE library that can be used to manage such events taking into account that the Activity that should process them could  not be visible or not even exist when the event occurs. More in details the GamesRoomEventAdapter converts an event into an Android broadcasted Intent that the Activity can receive. When working in “notification mode”, instead, it converts an event into a notification that is shown in the Android notification bar. It is the responsibility of the Activity to turn the notification mode on and off in its onPause() and onResume() methods respectively as shown by the code snippet below taken from the WelcomeActivity class.

@Override
protected void onResume() {
	// Check if there are pending invitations
	managePendingInvitations();

	// Then prepare to receive immediate notifications if an invitation is received while we are up
	incomingInvitationsListener = new BroadcastReceiver() {
		public void onReceive(Context ctx, Intent i) {
			managePendingInvitations();
		}
	};
	registerReceiver(incomingInvitationsListener, GamesRoomEventAdapter.getIntentFilter());

	// Be sure incoming invitations are notified as broadcasted intents and not as notifications
	GamesRoomEventAdapter adapter = ((MTris) getApplication()).getGamesRoomEventAdapter();
	adapter.setNotificationMode(false);

	super.onResume();
}

@Override
protected void onPause() {
	unregisterReceiver(incomingInvitationsListener);

	// Make incoming invitations be notified as notifications in the Android notification bar
	GamesRoomEventAdapter adapter = ((MTris) getApplication()).getGamesRoomEventAdapter();
	if (adapter != null) {
		adapter.setNotificationMode(true);
	}
	super.onPause();
}

private void managePendingInvitations() {
	GamesRoomEventAdapter adapter = ((MTris) getApplication()).getGamesRoomEventAdapter();
	if (adapter != null) {
		adapter.flushEvents(this);
	}
}

The flushEvents() method of the GamesRoomEventAdapter class called at line 36 makes all received events (if any) be processed by a GamesRoomFeature.Listener implementation (the WelcomeActivity class itself in our case). For each incoming join table proposal therefore the handleJoinTableProposal() method shown below is executed.

@Override
public void handleJoinTableProposal(final TableDescriptor td) {
	Log.i(MTris.TAG, "WelcomeActivity - Managing pending invitation from "+td.getOwner());
	// Check if invitation is still valid
	if (((MTris) getApplication()).getGamesRoomFeature().canJoin(td)) {
		AlertDialog.Builder dlgAlert  = new AlertDialog.Builder(this);
		String text = String.format(getResources().getString(R.string.welcome_incoming_invitation),
		                            AmuseUtils.getUserLabel(td.getOwner()));
		dlgAlert.setMessage(text);
		dlgAlert.setTitle(R.string.welcome_invitation);
		dlgAlert.setPositiveButton(R.string.welcome_accept, new DialogInterface.OnClickListener() {
			public void onClick(DialogInterface dialog, int which) {
				// If we are already in a table, leave it before
				((MTris) getApplication()).leaveCurrentTable();
				Intent i = new Intent(WelcomeActivity.this, MainActivity.class);
				Bundle b = new Bundle();
				b.putSerializable(MainActivity.TABLE_DESCRIPTOR, td);
				i.putExtras(b);
				startActivity(i);
			}
		});
		dlgAlert.setNegativeButton(R.string.welcome_reject, new DialogInterface.OnClickListener() {
			public void onClick(DialogInterface dialog, int which) {
				((MTris) getApplication()).getGamesRoomFeature().rejectJoinTableProposal(td);
			}
		});
		dlgAlert.setCancelable(true);
		dlgAlert.create().show();
		// If there are other pending events, avoid flushing them while the AlertDialog is shown
		((MTris) getApplication()).getGamesRoomEventAdapter().stopFlushing();
	}
	else {
		// Invitation expired
		Log.i(MTris.TAG, "WelcomeActivity - Managing expired invitation from "+td.getOwner());
	}
}

Many AMUSE components issue events and a suitable adapter (similar to the GamesRoomEventAdapter described above) is available for each type of event to conveniently manage them. For instance, when a player joined a table to play a match (as described in next section) all table related events (e.g. a new player joined, a player moved…) can be managed by means of the TableEventAdapter class.

 

5.3. Match organization

The GamesRoomFeature is based on the table metaphor. There are tables available and players can join them (figure A). As soon as the minimum number of players join a table (Figure B) the match can start on that table (Figure C). Tables are organized in rooms and a user must enter a room before he can join tables in that room.

GamesRoomA            GamesRoomB

GamesRoomC

The GamesRoomFeature class and related classes provide suitable methods to implement such mimics and to notify players about relevant events happening on the table they joined (e.g. another player joined/left, the match started, a player moved and so on). More in details the GamesRoomFeture class provides the entry point and allows listing rooms and entering a room. Each room is mapped by a Room object that on its turn allows listing tables, joining an existing table and creating a new table. Each table is mapped by a Table object that provides methods to move and (in case a player wants to abandon a match) leave the table. Furthermore the getGameSpecificData() and setGameSpecificData() methods allow associating to a table game specific information intended to keep the current status of a match as “seen” by each player.

To keep things simple, the MTris application is structured with a single room. Therefore the room entering step is carried out implicitly exploiting the doInRoom() method of the GamesRoomFeature class. This is a shortcut for calling enterRoom() and executing a piece of code (expressed as a Runnable object) when this completes. The code snippet below, taken from the MTris class shows how this is done.

public void joinTable(final Callback<Table> callback) {
	leaveCurrentTable();
	grf.doInRoom(ROOM_NAME, new Runnable() {
		public void run() {
			TableEventAdapter adapter = new TableEventAdapter();
			adapter.initialize(amuseClient, null);
			grf.getCurrentRoom().joinTable(adapter, callback);
		}
	}, callback);
}

At line 7, once inside the room, the joinTable() method of the Room class is used to join a free table. The grf variable is the GamesRoomFeature object. At lines 5 and 6 a TableEventAdapter is register to manage table related events. There is an overloaded version of the joinTable() method that allows specifying the table to join. In the MTris application this is used when an invitation to join a given table is received.

 

5.4. Match playing

The logics and graphics actually implementing MTris match playing are included in the MainActivity class. This class uses the TableEventAdapter mentioned in previous section to receive table related events similarly to what described in section 5.2 discussing the GamesRoomEventAdapter.

  • The handleMatchStarting() method is invoked when enough players joined the table (3 players in our case) and the match can start. The offset parameter is ignored in our case: it is useful for games that require synchronization between players. The data parameter is intended to notify game specific match startup information to the client. In the MTris game we exploit it to carry the name of the player who has to move first.
  • The handlePlayerJoined() method is invoked when a player joins the table the local player is currently in. In the MTris game this can only occur before the match starts. Other games may allow new players to join an ongoing match.
  • The handlePlayerLeft() method is invoked when a player leaves the table. This can occur both before and after match startup. In the second case, if the leaving player was the player expected to move, we exploit the data parameter to carry the indication about the new one.
  • The handlePlayerMoved() method is invoked when another player moved. The playerName parameter indicates the player who made the move (MTris is a turn-based game and therefore this will always be the currentPlayer). The data parameter allows carrying game-specific information about the move. In the MTris game we exploit it to disclose the space marked by the moving player. More in detail we use an object of the TurnPlayed class that keeps together the marked space, the score obtained by the moving player and the next player to move. As already mentioned the TurnPlayed class is defined in the server side part of the MTris application and is visible to the MTris client App since we included the MTris.jar file among the client libs.
  • The handleMatchEvent() method is invoked when a game specific event not directly related to a player move occurs. In the MTris game there are no such events and therefore the method is left empty.
  • The handleMatchFinished() method is invoked when the match finishes and carries the winners as well as some game specific information not exploited in our case.

When the local player touches a free space on the grid, if it is its turn, the move() method of the local Table is invoked (as shown in the code snippets below) to notify the server side logics of the MTris game. The latter notifies other players so that the handlePlayerMoved() method will be called in their MTris client Apps.

boardView.setOnItemClickListener(new OnItemClickListener() {
	@Override
	public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
		if (username.equals(matchInfo.getCurrentPlayer())) {
			Point p = toPoint(position);
			if (matchInfo.getBoard().isFree(p)) {
				int score = move(p);
				boardView.invalidateViews();
				if (score > 0) {
					updateScore(username, score);
					playersView.invalidateViews();
				}
			}
		}
	}
});
	private int move(Point p) {
		reproduceSound(R.raw.sound_move_me);
		final int score = matchInfo.getBoard().mark(username, p);
		setCurrentPlayer(null);
		stopTimer();
		Log.i(MTris.TAG, "MainActivity - Marked point "+p+". Score="+score);
		getTable().move(p, true, callbackManager.wrap(Object.class, new Callback<Object>() {
			@Override
			public void onSuccess(Object moveResult) {
				Log.i(MTris.TAG, "MainActivity - Move OK");
				TurnPlayed tp = (TurnPlayed) moveResult;
				if (tp.getScore() != score) {
					// FIXME: The local board is inconsistent with the centralized board --> Display an error 
					Log.w(MTris.TAG, "MainActivity - Board inconsistency: local-score=" + score + ", centralized-score=" + tp.getScore());
				} else {
					Log.i(MTris.TAG, "MainActivity - Next player is " + tp.getNextPlayer());
					setCurrentPlayer(tp.getNextPlayer());
					// Refresh players view only. 
					refreshUI(true, false, selectTitle(), null);
				}
			}

			@Override
			public void onFailure(Throwable th) {
				Log.w(MTris.TAG, "MainActivity - Move error: " + th.getMessage());
			}
		}));
		return score;
	}

6. Server-side logics

As already mentioned the GamesRoomFeature used by the MTris application to coordinate match organization and playing is based on a centralized approach and therefore requires some game-specific server-side logics to be developed. This section gives a brief description on the AMUSE APIs that allows to do that and outlines how to setup an Eclipse project suitable to develop such logics. Furthermore it shows how to deploy and run it on the AMUSE online platform.
Unlike the client part of an AMUSE application, understanding the server part (when needed) requires to be familiar with JADE and WADE.

6.1. AMUSE server-side API

The server side logics of an AMUSE-based application using the GamesRoomFeature is implemented as one or more instances of an agent extending the GamesRoomAgent class. More in details each agent instance implements a room. In the case of the MTris application where, to keep things as simple as possible, we decided to have a single room, there we start a single instance of the BoardAgent that extends GamesRoomAgent. The name of the agent corresponds to the name of the room.
Each GamesRoomAgent instance holds a number of TableManager objects each one representing a table inside the room.
Furthermore the GamesRoomAgent class provides a set of callback methods that are invoked when relevant events occur (e.g. a player joins a table or a player makes a move in a match that is being played in a table) and that must/can be redefined to implement game specific logics.

Tables can be created both automatically by means of the createTable() method of the GamesRoomAgent class or as a consequence of a request received from a user (triggered by a call to the createTable() method of the Room class occurred in a client). In both cases the handleTableCreated() callback method is invoked to allow initializing the newly created table.
The code snippet below taken from the BoardAgent class shows the agent initialization step, where 10 ready made tables are created, and the handleTableCreated() method where newly created tables are initialized.

@Override
public void agentSpecificSetup() throws AgentInitializationException {
	super.agentSpecificSetup();

	// Register game specific data types to be exchanged with the clients
	try {
		registerDataType(Point.class);
		registerDataType(TurnPlayed.class);
	}
	catch (Exception e) {
		throw new AgentInitializationException("Error registering data type", e);
	}

	// Create 10 ready-made tables (exactly 3 players per table)
	for (int i = 0; i < 10; i++) {
		createTable("table-"+i, 3, 3);
	}

	myLogger.log(Logger.INFO, "M-Tris Board agent "+getLocalName()+" - initialized");
}

@Override
public void handleTableCreated(TableManager table, Properties creationProps) {
	// A player cannot join an ongoing match even if there are less than 3 players (since one has left)
	table.setAllowJoinWhileOngoing(false);
	// A new table is being created. Initialize the status information
	myLogger.log(Logger.INFO, "M-Tris Board agent "+getLocalName()+" - initializing data for table "+table.getName());
	Board b = new Board();
	MTrisStatus status = new MTrisStatus();
	status.setBoard(b);
	table.setGameSpecificData(status);
}

Lines from 5 to 12 contains the code to initialize game specific data types that are exchanged with clients. This step is mostly identical to that described in section 5.1 and, of course, the registered data types are the same.

When creating a table it is possible to specify the minimum number of players required to start the match and the maximum number of player that can participate in a match. In the case of the MTris game a match is played by exactly 3 players and therefore both numbers are set to 3 (line 16). Furthermore, since a player can leave an ongoing match (thus leading to a situation where only two players participate in a match) we specify that new players cannot join an ongoing match (line 26). Finally (lines from 29 to 32) we create MTris specific structures (a Board object representing the MTris grid and an MTrisStatus object holding the board and other relevant information about the status of an MTris match) and we associate them to the table by means of the setGameSpecificData() method of the TableManager class.

The other relevant callback methods are listed below.

  • handleMatchStartup() - This is invoked as soon as the minimum number of players is reached and the match can start
  • handleMove() – This is invoked following a move performed by a player
  • handlePlayerJoined() – This is invoked when a player joins a table
  • handlePlayerLeft() – This is invoked when a player leaves a table

All these methods take

  • a TableManager object parameter representing the table where the event that triggered the method invocation occurred;
  • some parameters describing the occurred event. For instance the handleMove() method takes the name of the moving player and an Object describing the move itself;
  • an info parameter (specific for each method) that should be filled to specify if and what to notify to players after the event has been processed. For instance the handleMove() method takes a MoveInfo object where it is possible to specify the result of the move to the moving player and the effects of the move to other players (possibly different effects to different players).

The code snippet below shows how the handleMove() method is redefined in the BoardAgent class.

@Override
public void handleMove(TableManager table, String playerName, Object moveData, MoveInfo info)
		throws InvalidMoveException {
	// The current player has just moved
	MTrisStatus status = (MTrisStatus) table.getGameSpecificData();
	if (playerName.equals(status.getCurrentPlayer())) {
		// The player that was expected to move actually moved --> OK
		Point p = (Point) moveData;
		try {
			Board board = status.getBoard();
			int score = 0;
			if (p != null) {
				// Mark the indicated point in the board and increment the score of
				// the moving player
				score = board.mark(playerName, p);
				myLogger.log(Logger.INFO, "Agent "+getLocalName()+" - Table "+table.getName()+": Point "+p+" set to "+playerName+"["+board.getPlayerValue(playerName)+"]. Score="+score);
				status.incrementScore(playerName, score);
			}
			else {
				// Time expired for this player!
				myLogger.log(Logger.INFO, "Agent "+getLocalName()+" - Table "+table.getName()+": Time expired for player "+playerName+"["+board.getPlayerValue(playerName)+"]");
				status.timeExpired(playerName);
			}

			String nextPlayer = null;
			if (!board.isCompleted()) {
				// Select the next player
				status.selectCurrentPlayer();
				nextPlayer = status.getCurrentPlayer();
			}
			if (nextPlayer == null) {
				// Either board is completed (normal termination) or no player
				// can be selected (time expired for all players)
				table.setMatchFinished(status.getWinner(), null);
			}

			// Prepare move result and disclosed effects to other players:
			// marked point, # tris, nextPlayer.
			// This information is sent both to the moving player (as move result) and
			// to other players
			TurnPlayed tp = new TurnPlayed(p, score, nextPlayer);
			info.setResult(tp);
			info.setDisclosedData(tp);
		}
		catch (IllegalArgumentException iae) {
			myLogger.log(Logger.WARNING, "M-Tris Board agent "+getLocalName()+" - Invalid move received from player "+playerName+": point="+p+", error="+iae.getMessage());
			throw new InvalidMoveException(iae.getMessage());
		}
	}
	else {
		myLogger.log(Logger.WARNING, "M-Tris Board agent "+getLocalName()+" - Unexpected move received from player "+playerName+": currentPlayer="+status.getCurrentPlayer());
		throw new InvalidMoveException("Not your turn");
	}
}

When the board is complete the setMatchFinished() method of the TableManager class is invoked (line 34). This makes the GamesRoomAgent notify the match termination to all players in the table.

6.2. Setting up the Eclipse Project

In this section we describe how to setup the Eclipse project to develop the server-side logics (if any) of an AMUSE based game. Such logics is very similar to a WADE-based application. As a consequence in order to create it, it is necessary to setup an Eclipse Project as described in chapter 2 of the WADE Installation Guide. The only difference is related to the initial creation of the project default structure and therefore we suggest the following overall procedure.

  1. Follow steps 1 to 5 described in Chapter 2 of the WADE Installation Guide. These lead you to the creation of a Wade Project for your code in Eclipse.
  2. Download the Amuse Platform distribution package and unzip it somewhere on your disk (this will produce a directory structure similar to that below
    ...
     |--amuse/
          |--...
          |--platform/
                |--cfg/
                |--lib/
                |--log/
                |--src/
                |--tools/
                |--utility/
                |--...
                |--build.properties
                |--build.xml
    
  3. Setup the project default structure as below:
    - Copy the build.xml and build.properties files from the utility/ directory (NOTE: Not those included in the platform/ directory) of your local AMUSE installation to the home directory of your project.
    - Edit the build.properties file specifying the name and version of your application and the locations of WADE and AMUSE as exemplified below.

    # Application name
    application-name=MyAmuseApp
    # Application version
    version=1.0
    # The directory where WADE is installed
    wade-home=C:/develop/wadeSuite/wade
    # The directory where AMUSE Platform is installed
    amuse-home=C:/develop/amuse/platform
    

    - Open a shell, move to the home directory of your project and type
    ant setup
    This requires Apache ANT 1.7 or later.

  4. Configure your project as described in step 6 of Chapter 2 of the WADE Installation Guide, without making Wolf create the project default structure for you: the correct structure was already created in previous step.

6.3. Running the server side logics on the AMUSE online platform

Once the server side logics of your application is ready, you can create the installation package ready to be deployed on the AMUSE online platform.
Assuming you have copied the build.xml and build.properties files from the utility/ directory of your local AMUSE installation to the home directory of your project and you have properly edited the build.properties file as described in previous section, open a shell, move to the home directory of your project and type

ant rebuild

This requires Apache ANT 1.7 or later.
The installation package will be produced in the dist/ sub-directory of your project.
The produced installation package can be installed, configured and activated by means of the AMUSE Administration Console.
The configuration step implies specifying, by means of a suitable xml file, which agents to activate server-side. The snippet below shows the xml configuration file used for the MTris server part.

<?xml version="1.0" encoding="UTF-8"?>
<Application>
  <agents>
    <Agent className="mTris.BoardAgent" name="room1" />
    <Agent className="mTris.virtualPlayer.VPAgent" name="Valerio" type="Virtual Player Agent">
    	<parameters>
    		<Parameter key="watchdogTimeout" value="60000"/>
    	</parameters>
    </Agent>
    <Agent className="mTris.virtualPlayer.VPAgent" name="Vaffa89" type="Virtual Player Agent">
    	<parameters>
    		<Parameter key="watchdogTimeout" value="120000"/>
    	</parameters>
    </Agent>
    <Agent className="mTris.virtualPlayer.VPAgent" name="Vincenzo" type="Virtual Player Agent">
    	<parameters>
    		<Parameter key="watchdogTimeout" value="120000"/>
    	</parameters>
    </Agent>
    <Agent className="mTris.virtualPlayer.VPAgent" name="Vanessa" type="Virtual Player Agent">
    	<parameters>
    		<Parameter key="watchdogTimeout" value="120000"/>
    	</parameters>
    </Agent>
    <Agent className="mTris.virtualPlayer.VPAgent" name="Vicky" type="Virtual Player Agent">
    	<parameters>
    		<Parameter key="watchdogTimeout" value="120000"/>
    	</parameters>
    </Agent>
  </agents>
</Application>

Line 4 specifies to start an agent called room1 of class mTris.BoardAgent. This is the agent implementing the only room of the MTris game.
Successive lines specify to start some agents representing virtual players. How to develop virtual players is not addressed by this tutorial.