77. Implementing a chess clock

Starting with JDK 8, the java.time package has an abstract class named Clock. The main purpose of this class is to allow us to plug in different clocks when needed (for example, for testing purposes). By default, Java comes with four implementations: SystemClock, OffsetClock, TickClock, and FixedClock. For each of these implementations, there are static methods in the Clock class. For example, the following code creates FixedClock (a clock that always returns the same Instant):

Clock fixedClock = Clock.fixed(Instant.now(), ZoneOffset.UTC);

There is also TickClock, which returns the current Instant ticking in whole seconds for the given time zone:

Clock tickClock = Clock.tickSeconds(ZoneId.of("Europe/Bucharest"));
There is also a method that can be used to tick in whole minutes, tickMinutes(), and a generic one, tick(), which allows us to specify Duration.

A Clock class may also support time zones and offsets, but the most important method of a Clock class is instant(). This method returns the instant of Clock:

// 2019-03-01T13:29:34Z
System.out.println(tickClock.instant());
There is also the millis() method, which returns the current instant of the clock in milliseconds.

Let's assume that we want to implement a clock that acts a chess clock:

In order to implement a Clock class, there are several steps to follow:

  1. Extend the Clock class.
  2. Implement Serializable.
  3. Override at least the abstract methods inherited from Clock.

A skeleton of a Clock class is as follows:

public class ChessClock extends Clock implements Serializable {

@Override
public ZoneId getZone() {
...
}

@Override
public Clock withZone(ZoneId zone) {
...
}

@Override
public Instant instant() {
...
}
}

Our ChessClock will work only with UTC; no other time zone will be supported. This means that the getZone() and withZone() methods can be implemented as follows (of course, this can be modified in the future):

@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}

@Override
public Clock withZone(ZoneId zone) {
throw new UnsupportedOperationException(
"The ChessClock works only in UTC time zone");
}

The climax of our implementation is the instant() method. The difficulty consists in managing two Instant, one for the player from the left (instantLeft) and one for the player from the right (instantRight). We can associate every call of the instant() method with the fact that the current player has performed a move, and now it is the other player's turn. So, basically, this logic says that the same player cannot call instant() twice. Implementing this logic, the instant() method is as follows:

public class ChessClock extends Clock implements Serializable {

public enum Player {
LEFT,
RIGHT
}

private static final long serialVersionUID = 1L;

private Instant instantStart;
private Instant instantLeft;
private Instant instantRight;
private long timeLeft;
private long timeRight;
private Player player;

public ChessClock(Player player) {
this.player = player;
}

public Instant gameStart() {

if (this.instantStart == null) {
this.timeLeft = 0;
this.timeRight = 0;
this.instantStart = Instant.now();
this.instantLeft = instantStart;
this.instantRight = instantStart;
return instantStart;
}

throw new IllegalStateException(
"Game already started. Stop it and try again.");
}

public Instant gameEnd() {

if (this.instantStart != null) {
instantStart = null;
return Instant.now();
}

throw new IllegalStateException("Game was not started.");
}

@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}

@Override
public Clock withZone(ZoneId zone) {
throw new UnsupportedOperationException(
"The ChessClock works only in UTC time zone");
}

@Override
public Instant instant() {

if (this.instantStart != null) {
if (player == Player.LEFT) {
player = Player.RIGHT;

long secondsLeft = Instant.now().getEpochSecond()
- instantRight.getEpochSecond();
instantLeft = instantLeft.plusSeconds(
secondsLeft - timeLeft);
timeLeft = secondsLeft;

return instantLeft;
} else {
player = Player.LEFT;

long secondsRight = Instant.now().getEpochSecond()
- instantLeft.getEpochSecond();
instantRight = instantRight.plusSeconds(
secondsRight - timeRight);
timeRight = secondsRight;

return instantRight;
}
}

throw new IllegalStateException("Game was not started.");
}
}

So, depending on which player calls the instant() method, the code computes the number of seconds needed by that player to think until she/he performed a move. Moreover, the code switches the player, so the next call of instant() will deal with the other player.

Let's consider a chess game starting at 2019-03-01T14:02:46.309459Z:

ChessClock chessClock = new ChessClock(Player.LEFT);

// 2019-03-01T14:02:46.309459Z
Instant start = chessClock.gameStart();

Further, the players perform the following sequence of movements until the player from the right wins the game:

Left moved first after 2 seconds: 2019-03-01T14:02:48.309459Z
Right moved after 5 seconds: 2019-03-01T14:02:51.309459Z
Left moved after 6 seconds: 2019-03-01T14:02:54.309459Z
Right moved after 1 second: 2019-03-01T14:02:52.309459Z
Left moved after 2 second: 2019-03-01T14:02:56.309459Z
Right moved after 3 seconds: 2019-03-01T14:02:55.309459Z
Left moved after 10 seconds: 2019-03-01T14:03:06.309459Z
Right moved after 11 seconds and win: 2019-03-01T14:03:06.309459Z

It looks like the clock has correctly registered the movements of the players.

Finally, the game is over after 40 seconds:

Game ended:2019-03-01T14:03:26.350749300Z
Instant end = chessClock.gameEnd();

Game duration: 40 seconds
// Duration.between(start, end).getSeconds();
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.26.138