1: <?php
2: /*
3: * Copyright (c) 2016 Geraldo B. Landre
4: *
5: * See the file LICENSE for copying permission.
6: */
7: namespace pgn;
8:
9: use pgn\tags\Tag;
10: use pgn\exceptions\InvalidGameFormatException;
11:
12: /**
13: * Description of Game:
14: * A PGN Game is composed by a set of tags (at least STR) and a movetext section:
15: * The movetext section is composed of chess moves, move number indications,
16: * optional annotations, and a single concluding game termination marker.
17: *
18: * @todo Because illegal moves are not real chess moves, they are not permitted in PGN
19: * movetext. They may appear in commentary, however. One would hope that illegal
20: * moves are relatively rare in games worthy of recording.
21: *
22: * Game Termination Markers:
23: *
24: * Each movetext section has exactly one game termination marker; the marker
25: * always occurs as the last element in the movetext. The game termination marker
26: * is a symbol that is one of the following four values: "1-0" (White wins), "0-1"
27: * (Black wins), "1/2-1/2" (drawn game), and "*" (game in progress, result
28: * unknown, or game abandoned). Note that the digit zero is used in the above;
29: * not the upper case letter "O". The game termination marker appearing in the
30: * movetext of a game must match the value of the game's Result tag pair. (While
31: * the marker appears as a string in the Result tag, it appears as a symbol
32: * without quotes in the movetext.)
33: * @see pgn_standard.txt
34: * @author Geraldo
35: */
36: class Game {
37:
38: /**
39: *
40: * @var Tag [] array of tags
41: */
42: private $tags;
43:
44: /**
45: *
46: * @var string
47: */
48: private $moveText;
49:
50: /**
51: *
52: * @var string [] array
53: */
54: private $parseErrors;
55:
56: /**
57: * Creates an empty Game
58: */
59: public function __construct() {
60: $this->moveText = "";
61: $this->tags = array();
62: $this->parseErrors = array();
63: }
64:
65: /**
66: * Returns a string representation of the Game object
67: * @return string a string representation of the PGN object
68: */
69: public function __toString() {
70: return $this->toString();
71: }
72:
73: /**
74: * Returns a string representation of the Game object
75: * @todo implement support of a properties file with the property defaultEndLine
76: * @param string $endl The end-line character (default: "\n").
77: * Example:
78: * <pre><code>
79: * <?php
80: * $object = new Game;
81: * $object->toString("<br />");
82: * </code></pre>
83: * @return string a string representation of the PGN object
84: */
85: public function toString($endLine = "\n") {
86:
87: if (empty($this->moveText) && count($this->tags) == 0) {
88: return "";
89: }
90:
91: $strTags = "";
92: foreach($this->tags as $tag) {
93: $strTags .= $tag . $endLine;
94: }
95:
96: return $strTags . $endLine . $endLine . $this->moveText;
97: }
98:
99: /**
100: * Parses a string containing a game and fills movetext and tags attributes.
101: * The tag attribute is filled as an associative array (TagName => TagObject)
102: *
103: * @assert(NULL) throws pgn\exceptions\InvalidGameFormatException
104: * @assert(123) throws pgn\exceptions\InvalidGameFormatException
105: *
106: * @param string $unparsedGame string containing one PGN Game
107: * @throws InvalidGameFormatException throws an exception if the parameter
108: * is not a string or if it doesn't have the correct fields, i.e.
109: * the move text and the seven roster tags
110: */
111: public function parse($unparsedGame) {
112: if (!is_string($unparsedGame)) {
113: throw new InvalidGameFormatException("[" . __CLASS__ . "] invalid game format (it's not a valid string): " . $unparsedGame);
114: }
115:
116: $parsing = explode("]", $unparsedGame);
117:
118: if(count($parsing) < 8) {
119: throw new InvalidGameFormatException("[" . __CLASS__ . "] invalid game format (it doesn't have correct fields): " . $unparsedGame);
120: }
121:
122: $uncheckedMoveText = array_pop($parsing);
123:
124: foreach ($parsing as $unparsedTag) {
125: $tag = Tag::parse($unparsedTag . "]", $this->parseErrors);
126: $this->addTag($tag);
127: }
128:
129: if (!$this->checkSetUpAndFEN()) {
130: throw new InvalidGameFormatException("[" . __CLASS__ . "] invalid game format (SetUp=1 without FEN): " . $unparsedGame);
131: }
132:
133: if ($this->checkMoveText($uncheckedMoveText)) {
134: $this->moveText = $uncheckedMoveText;
135: } else {
136: $this->parseErrors[] = "[" . __CLASS__ . "] MoveText: " . $uncheckedMoveText;
137: }
138: }
139:
140: /**
141: *
142: * @param string $tagName
143: * @return Tag
144: */
145: public function getTag($tagName) {
146: if(array_key_exists($tagName, $this->tags)) {
147: return $this->tags[$tagName];
148: }
149: return NULL;
150: }
151:
152: /**
153: *
154: * @param Tag $tag
155: */
156: public function addTag($tag) {
157: if ($tag instanceof Tag) {
158: $this->tags[$tag->getName()] = $tag;
159: }
160: }
161:
162: /**
163: * @todo implement this method
164: * @param string $uncheckedMoveText
165: * @return boolean
166: */
167: protected function checkMoveText($uncheckedMoveText) {
168: return true;
169: }
170:
171: protected function checkSetUpAndFEN() {
172: $setUp = $this->getTag("SetUp");
173: $fen = $this->getTag("FEN");
174:
175: // the pair SetUp and FEN must exists together
176: if($setUp === NULL) {
177: return $fen === NULL;
178: }
179: else {
180: return $fen !== NULL;
181: }
182: }
183:
184: }
185: