ovcc-game.vala 17.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/* 
 * 
 * Copyright (C) 2009-2011 Colomban Wendling <ban@herbesfolles.org>
 *                         Jonathan Michalon <studios.chalmion@no-log.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

namespace OVCC
{
  /**
24
   * The errors of the GameError domain.
25
   * 
26 27 28 29
   * || STARTED              || The game is started and the action therefore cannot be done. ||
   * || PLAYER_ALREADY_ADDED || Player is already on the game.                               ||
   * || DUPLICATED_NICK      || Player have the same nick than an another player.            ||
   * || FAILED               || Something failed...                                          ||
30 31 32 33 34 35 36 37 38 39
   */
  public errordomain GameError
  {
    STARTED,
    PLAYER_ALREADY_ADDED,
    DUPLICATED_NICK,
    FAILED
  }

  /**
40
   * Possible states of a game.
41
   * Only one state at a time. "Flags" type is used for filtering only.
42
   * 
43 44 45 46 47
   * || NEW            || The game is just created but no player joined ||
   * || PLAYER_WAITING || The game has players but is not started yet   ||
   * || STARTED        || The game is started                           ||
   * || FINISHED       || The game is finished                          ||
   * || ABORTED        || The game is stopped, but not finished         ||
48
   */
49
  [Flags]
50 51
  public enum GameState
  {
52 53
    NEW,
    PLAYER_WAITING,
54
    STARTED,
55
    FINISHED,
56 57 58 59
    ABORTED;

    public bool is_open()
    {
60
      return this in (NEW | PLAYER_WAITING);
61 62 63
    }
    public bool is_done()
    {
64
      return this in (FINISHED | ABORTED);
65 66 67
    }
    public bool can_start()
    {
68
      return this in PLAYER_WAITING;
69
    }
70 71
  }

72 73 74 75
  /**
   * A class providing the description of a Game.
   * This is a stripped-down group of data typically to descibe a Game over network.
   */
76
  [Compact]
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
  public class GameDescription : Object
  {
    public string     name          { get; construct; }
    public GameState  state         { get; construct; }
    public string[]   player_nicks  { get; construct; }

    public GameDescription (string name, GameState state, string[] player_nicks)
    {
        Object(name: name, state: state, player_nicks: player_nicks);
    }

    public string to_string ()
    {
      return "Game '%s' in '%s' state with players '%s'".printf (name,
                                                                 state.to_string(),
                                                                 string.joinv(", ", player_nicks));
    }
  }

96 97 98 99
  /**
   * The main part of OVCC. This is the glue connecting all the game's parts to
   * make a playable game, and play it.
   */
100 101
  public class Game : Object
  {
102 103
    private SList<Player> _players = null;
    
104 105
    // FIXME implement UUIDs for reference over network instead of mere list index
    // public string                 uuid            { get; construct; default = Uuid.string_random(); }
106
    public GameState              state           { get; private set; default = GameState.NEW; }
107 108
    public Stack                  stack           { private get; construct; }
    public Board                  board           { get; construct; }
109
    public unowned SList<Player>  players         { get { return this._players; } }
110
    public Player                 current_player  { get; private set; }
111
    public unowned Tile           current_tile {
112 113
      get { return stack.peek(); }
    }
114
    public uint                   n_tiles_left {
115 116 117 118 119 120
      get { return stack.size; }
    }

    public signal void player_added     (Player p);
    public signal void player_removed   (Player p);
    public signal void unplaceable_tile (Tile t);
121 122 123 124
    /**
     * A signal to be emitted when a player finished its turn
     */
    public signal void turn_finished ();
125 126


127
    public Game (TileSet ts, Stack? s = null)
128
    {
129
      var our_stack = s;
130 131 132
      if (ts.first == null) {
        critical ("Given tileset has no first tile");
      }
133 134 135
      if (s == null) {
        our_stack = new Stack.from_tileset (ts);
      }
136
      Object (stack: our_stack, board: new Board (ts.first.dup ()));
137 138 139 140

      stack.item_removed.connect (t => {
          this.notify_property ("current-tile");
          if (stack.is_empty ()) {
141
            state = GameState.FINISHED;
142 143 144
          }
        });
      this.notify["current-tile"].connect (() => {
145
          if (state == GameState.STARTED && current_tile != null &&
146 147 148 149 150
              ! board.is_tile_placeable (current_tile)) {
            unplaceable_tile (current_tile);
            stack.pop ();
          }
        });
151 152
      this.player_added.connect ((p) => {
          this.notify_property ("players");
153 154 155
          if (players.length() == 1 && this.state == GameState.NEW) {
            this.state = GameState.PLAYER_WAITING;
          }
156 157 158
        });
      this.player_removed.connect ((p) => {
          this.notify_property ("players");
159 160 161
          if (players.length() < 1 && this.state == GameState.PLAYER_WAITING) {
            this.state = GameState.NEW;
          }
162
        });
163
      this.turn_finished.connect (() => next_player ());
164 165 166 167 168 169 170 171 172 173

      /* handle intermediate point count (when turn finished) */
      this.turn_finished.connect (() => after_turn_finished ());

      /* handle final point count */
      this.notify["state"].connect (() => {
          if (state == GameState.FINISHED) {
            after_game_finished();
          }
        });
174
    }
175
    
176
    /**
177
     * Starts a game
178
     * 
179
     * This needs at least two players to be on the game to work.
180
     * 
181
     * @return Whether the game is started (and not whether game just started)
182 183
     */
    public bool start ()
184
      throws GameError
185
    {
186 187
      if (state == GameState.STARTED) {
        throw new GameError.STARTED ("Game already started");
188
      }
Jonathan Michalon's avatar
Jonathan Michalon committed
189 190 191

      state = GameState.STARTED;

192 193 194 195
      /* set starting player as the first in the list */
      current_player = _players.nth_data(0);

      return true;
196
    }
197
    
198
    /**
199
     * Aborts the game
200 201 202 203 204 205 206
     */
    public void abort ()
    {
      if (state == GameState.STARTED) {
        state = GameState.ABORTED;
      }
    }
207
    
208 209 210 211 212
    /* change player to the next one */
    private void next_player ()
    {
      bool   pick_next = false;
      Player first     = null;
213
      foreach (var item in _players) {
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
        if (first == null) {
          first = item;
        }
        if (pick_next) {
          current_player = item;
          pick_next = false;
          break;
        }
        if (item == current_player) {
          pick_next = true;
        }
      }
      if (pick_next) {
        /* here: didn't got a player next to current, so return the first */
        current_player = first;
      }
    }
231
    
232 233 234
    /**
     * Tries to place the current tile.
     * 
235 236 237 238 239 240
     * To place the current tile, the position bust be valid and the player must
     * be the one of which it is the turn to play.
     * 
     * @param player the player that places the tile
     * @param pos the position at which place the tile
     * @return true of the tile was correctly placed, false otherwise
241 242
     */
    public bool place_tile (Player    player,
243
                            Position  pos)
244 245 246 247
    {
      bool placed = false;
      
      if (state == GameState.STARTED && player == current_player) {
248
        placed = board.add_tile (stack.peek(), pos);
249 250 251 252
      }
      
      return placed;
    }
253

254 255 256 257 258 259 260 261 262 263 264 265 266
    /* checks to run after a turn was finished (mainly pawn removal) */
    private void after_turn_finished ()
    {
      var pos  = board.last_position;
      var tile = board.get_tile(pos);
      /* check whether city or path completed */
      foreach (var o in tile.objects) {
        switch (o.otype) {
          case TileObjectType.PATH:
          case TileObjectType.CITY:
            bool complete;
            var components = board.get_connected (o, pos, out complete);
            if (complete) {
267
              make_score_city_path_fields (components, complete);
268 269 270 271 272 273 274 275 276
            }
            break;
        }
      }
      /* check for a completed monastery around */
      for (var x = pos.x-1; x <= pos.x+1; x++) {
        for (var y = pos.y-1; y <= pos.y+1; y++) {
          var t = board.get_tile ({x, y});
          if (t != null) {
277
            /* search for a monastery on the tile */
278 279
            foreach (var o in t.objects) {
              if (o.otype == TileObjectType.MONASTERY) {
280 281 282 283 284 285 286 287 288
                /* ensure it is occupied */
                if (o.occupant != null) {
                  bool ok = true;
                  /* check whether it is complete */
                  for (var mx = x-1; ok && mx <= x+1; mx++) {
                    for (var my = y-1; ok && my <= y+1; my++) {
                      if (!board.is_tile_there ({mx, my})) {
                        ok = false;
                      }
289 290
                    }
                  }
291 292 293 294
                  /* if complete, score */
                  if (ok) {
                    make_score_monastery (o, {x, y}, true);
                  }
295 296 297 298 299 300
                }
              }
            }
          }
        }
      }
301 302 303
      
      /* pop the next tile on stack */
      stack.pop ();
304 305 306 307 308
    }

    /* do work when game finished (count points) */
    private void after_game_finished ()
    {
309
      var visited_cities = new SList<TileObject> ();
310 311 312
      board.foreach ((b, p, t) => {
          foreach (var o in t.objects) {
            if (o.occupant != null) {
313 314 315 316 317
              switch (o.otype) {
                case TileObjectType.PATH:
                case TileObjectType.CITY:
                  bool complete;
                  var components = board.get_connected (o, p, out complete);
318
                  make_score_city_path_fields (components, complete);
319 320 321 322 323 324 325 326
                  break;
                case TileObjectType.MONASTERY:
                  make_score_monastery (o, p, false);
                  break;
                case TileObjectType.FIELD:
                  /* TODO */
                  break;
              }
327
            }
328 329 330 331 332 333 334 335 336 337 338 339
            /* fields */
            if (o.otype == TileObjectType.CITY && visited_cities.find (o) == null) {
              SList<TileObject> city;
              SList<TileObject> fields;
              var complete = board.get_city_with_fields (o, p, out city, out fields);
              foreach (var c in city) {
                visited_cities.prepend (c);
              }
              if (complete) {
                make_score_city_path_fields (fields, false);
              }
            }
340 341 342 343
          }
          return true;
        });
    }
344 345 346 347
    
    /* handel linked components for scoring -- paths & cities
     * makes the players score according to the rules
     * removes the pawns */
348 349
    private void make_score_city_path_fields (SList<TileObject> components,
                                              bool complete)
350 351 352 353 354 355
    {
      /* nonsense on empty list */
      assert (components.length() != 0);

      uint n      = 0;  /* number of objects */
      uint counts = 0;  /* scoring ticks */
356 357
      var  list   = new SList<Player>();
      var  table  = new HashTable<Player, uint>(null, null);
358 359 360 361 362 363 364 365 366 367
      /* loop on each TileObject */
      foreach (var o in components) {
        counts ++;
        n++;
        /* handle bonus */
        if ((o.attributes & TileObjectAttributes.BONUS) != 0) {
          counts ++;
        }
        /* handle occupied objects */
        if (o.occupant != null) {
368 369 370 371 372 373 374
          uint power;
          if (! table.lookup_extended (o.occupant.player, null, out power)) {
            /* if the player wasn't in @table already, init its power and
             * add it to the list */
            power = 0;
            list.prepend (o.occupant.player);
          }
375 376
          switch (o.occupant.kind) {
            case PawnKind.NORMAL:
377
              power++;
378 379
              break;
            case PawnKind.DOUBLE:
380
              power += 2;
381 382
              break;
          }
383
          table.insert (o.occupant.player, power);
384
          /* removing pawn now is efficient and not wrong */
385
          if (components.nth_data(0).otype != TileObjectType.FIELD) {
386
            board.remove_pawn (o.occupant);
387
          }
388 389
        }
      }
390 391 392 393 394
      uint max_power = 0;
      /* find out what power is needed to score */
      foreach (var player in list) {
        max_power = uint.max (max_power, table.lookup (player));
      }
395
      /* check that we actually found someone */
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
      if (max_power > 0) {
        /* make each player with enough power actually score */
        foreach (var player in list) {
          if (table.lookup (player) == max_power) {
            /* handle small cities */
            if (n == 2) {
              player.score += 2;
            } else {
              /* score according to type */
              switch (components.nth_data(0).otype) {
              case TileObjectType.CITY:
                /* TODO change when end */
                player.score += counts * 2;
                break;
              case TileObjectType.PATH:
                /* TODO change when end */
                player.score += counts;
                break;
              case TileObjectType.FIELD:
                player.score += 3;
                break;
              }
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
            }
          }
        }
      }
    }

    /* handle scoring when on a monastery */
    private void make_score_monastery (TileObject object,
                                       Position pos,
                                       bool complete)
    {
      assert (object.otype == TileObjectType.MONASTERY);
      if (object.occupant != null) {
        if (!complete) {
          for (var x = pos.x-1; x <= pos.x+1; x++) {
            for (var y = pos.y-1; y <= pos.y+1; y++) {
              if (this.board.is_tile_there ({x, y})) {
                object.occupant.player.score++;
              }
            }
          }
        } else {
          object.occupant.player.score += 9;
        }
442
        board.remove_pawn (object.occupant);
443 444
      }
    }
445

446 447 448 449 450 451 452 453 454 455
    /**
     * Tries to place a pawn on the given TileObject
     *
     * To succeed the current player must be the owner of the pawn and
     * the object must be free (ie. no other pawn on the same "object").
     * By "object" it is meant the given TileObject AND all of its neighbours,
     * doing recursion on the tiles
     *
     * @param pawn The concerned pawn
     * @param object The TileObject where to place the pawn
456 457
     * @param pos The position where the object's tile is located on the board
     * @return true if the pawn was correctly placed, false otherwise
458
     */
459
    public bool place_pawn (Pawn pawn, TileObject object, Position pos)
460
    {
461
      if (! current_player.has_pawn (pawn)) {
462
        return false;
463
      }
464

465
      return board.add_pawn (pawn, object, pos);
466
    }
467
    
468
    /**
469
     * Tries to add a player to a game.
470
     * 
471
     * Note that the game must not be started.
472
     * 
473 474 475
     * @param player the player to add to the game
     * @return true if the player was successfully added to the game, false
     *         otherwise.
476 477 478 479 480 481 482 483 484
     */
    public bool add_player (Player player)
      throws GameError
    {
      bool success = false;
      
      if (state == GameState.STARTED) {
        throw new GameError.STARTED ("Game should not be started");
      } else {
485
        foreach (var item in _players) {
486 487 488 489 490 491 492 493
          if (item == player) {
            throw new GameError.PLAYER_ALREADY_ADDED
              ("Player \"%s\" already on the game", player.nick);
          } else if (item.nick == player.nick) {
            throw new GameError.DUPLICATED_NICK
              ("Nick \"%s\" already on the game", player.nick);
          }
        }
494
        _players.append (player);
495
        player.joined (this);
496 497 498 499 500 501
        player_added (player);
        success = true;
      }
      
      return success;
    }
502
    
503
    /**
504
     * Tries to remove a player from a game.
505
     * 
506 507 508
     * @param player the player to remove from the game
     * @return true if the player was successfully removed from the game, false
     *         otherwise.
509 510 511 512 513 514
     */
    public bool remove_player (Player player)
    {
      bool  success = false;
            
      /* can a player leave a running game? */
515
      if (players.find (player) == null) {
516
        warning ("Player \"%s\" is not on this game", player.nick);
517
      } else {
518
        _players.remove (player);
519 520 521 522 523 524
        player_removed (player);
        success = true;
      }
      
      return success;
    }
525 526 527 528 529 530 531 532 533 534 535

    public uint[] get_stack_ids ()
    {
      return stack.get_ids();
    }
    public string[] get_player_nicks ()
    {
      string[] nicks = new string[players.length()];
      int i = 0;
      foreach (Player p in players) {
        nicks[i] = p.nick;
536
        i++;
537 538 539
      }
      return nicks;
    }
540 541 542 543 544 545 546 547

    /**
     * Generate the {@link GameDescription} corresponding to this Game instance.
     *
     * @return A {@link GameDescription} describing us.
     */
    public GameDescription describe ()
    {
548
      /* FIXME game name */
549 550
      return new GameDescription ("Unnamed Game", state, get_player_nicks());
    }
551 552
  }
}