ovcc-game.vala 17 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 60 61 62 63 64 65 66 67 68 69
    ABORTED;

    public bool is_open()
    {
      return NEW in this || PLAYER_WAITING in this;
    }
    public bool is_done()
    {
      return FINISHED in this || ABORTED in this;
    }
    public bool can_start()
    {
      return PLAYER_WAITING in this;
    }
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
    public GameState              state           { get; private set; default = GameState.NEW; }
105 106
    public Stack                  stack           { private get; construct; }
    public Board                  board           { get; construct; }
107
    public unowned SList<Player>  players         { get { return this._players; } }
108
    public Player                 current_player  { get; private set; }
109
    public unowned Tile           current_tile {
110 111
      get { return stack.peek(); }
    }
112
    public uint                   n_tiles_left {
113 114 115 116 117 118
      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);
119 120 121 122
    /**
     * A signal to be emitted when a player finished its turn
     */
    public signal void turn_finished ();
123 124


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

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

      /* 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();
          }
        });
172
    }
173
    
174
    /**
175
     * Starts a game
176
     * 
177
     * This needs at least two players to be on the game to work.
178
     * 
179
     * @return Whether the game is started (and not whether game just started)
180 181
     */
    public bool start ()
182
      throws GameError
183
    {
184 185
      if (state == GameState.STARTED) {
        throw new GameError.STARTED ("Game already started");
186
      }
Jonathan Michalon's avatar
Jonathan Michalon committed
187 188 189

      state = GameState.STARTED;

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

      return true;
194
    }
195
    
196
    /**
197
     * Aborts the game
198 199 200 201 202 203 204
     */
    public void abort ()
    {
      if (state == GameState.STARTED) {
        state = GameState.ABORTED;
      }
    }
205
    
206 207 208 209 210
    /* change player to the next one */
    private void next_player ()
    {
      bool   pick_next = false;
      Player first     = null;
211
      foreach (var item in _players) {
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
        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;
      }
    }
229
    
230 231 232
    /**
     * Tries to place the current tile.
     * 
233 234 235 236 237 238
     * 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
239 240
     */
    public bool place_tile (Player    player,
241
                            Position  pos)
242 243 244 245
    {
      bool placed = false;
      
      if (state == GameState.STARTED && player == current_player) {
246
        placed = board.add_tile (stack.peek(), pos);
247 248 249 250
      }
      
      return placed;
    }
251

252 253 254 255 256 257 258 259 260 261 262 263 264
    /* 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) {
265
              make_score_city_path_fields (components, complete);
266 267 268 269 270 271 272 273 274
            }
            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) {
275
            /* search for a monastery on the tile */
276 277
            foreach (var o in t.objects) {
              if (o.otype == TileObjectType.MONASTERY) {
278 279 280 281 282 283 284 285 286
                /* 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;
                      }
287 288
                    }
                  }
289 290 291 292
                  /* if complete, score */
                  if (ok) {
                    make_score_monastery (o, {x, y}, true);
                  }
293 294 295 296 297 298
                }
              }
            }
          }
        }
      }
299 300 301
      
      /* pop the next tile on stack */
      stack.pop ();
302 303 304 305 306
    }

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

      uint n      = 0;  /* number of objects */
      uint counts = 0;  /* scoring ticks */
354 355
      var  list   = new SList<Player>();
      var  table  = new HashTable<Player, uint>(null, null);
356 357 358 359 360 361 362 363 364 365
      /* 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) {
366 367 368 369 370 371 372
          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);
          }
373 374
          switch (o.occupant.kind) {
            case PawnKind.NORMAL:
375
              power++;
376 377
              break;
            case PawnKind.DOUBLE:
378
              power += 2;
379 380
              break;
          }
381
          table.insert (o.occupant.player, power);
382
          /* removing pawn now is efficient and not wrong */
383
          if (components.nth_data(0).otype != TileObjectType.FIELD) {
384
            board.remove_pawn (o.occupant);
385
          }
386 387
        }
      }
388 389 390 391 392
      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));
      }
393
      /* check that we actually found someone */
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
      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;
              }
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
            }
          }
        }
      }
    }

    /* 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;
        }
440
        board.remove_pawn (object.occupant);
441 442
      }
    }
443

444 445 446 447 448 449 450 451 452 453
    /**
     * 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
454 455
     * @param pos The position where the object's tile is located on the board
     * @return true if the pawn was correctly placed, false otherwise
456
     */
457
    public bool place_pawn (Pawn pawn, TileObject object, Position pos)
458
    {
459
      if (! current_player.has_pawn (pawn)) {
460
        return false;
461
      }
462

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

    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;
534
        i++;
535 536 537
      }
      return nicks;
    }
538 539 540 541 542 543 544 545

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