bga-dev-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CC_PLAN.md +781 -0
- package/README.md +59 -0
- package/SKILL.md +117 -0
- package/composer.json +13 -0
- package/harness/example/SampleGame.php +160 -0
- package/harness/example/SampleGameTest.php +104 -0
- package/harness/example/sampleUtils.test.js +53 -0
- package/harness/js/bgaStubs.js +32 -0
- package/harness/js/testHelpers.js +9 -0
- package/harness/php/BgaDatabaseFake.php +234 -0
- package/harness/php/BgaExceptionTypes.php +17 -0
- package/harness/php/BgaGameTestCase.php +160 -0
- package/harness/php/BgaNotificationSpy.php +116 -0
- package/harness/php/BgaStubs.php +244 -0
- package/jest.config.js +5 -0
- package/package.json +11 -0
- package/skills/database-patterns.md +143 -0
- package/skills/js-dojo-patterns.md +89 -0
- package/skills/notifications.md +321 -0
- package/skills/scaffold-templates.md +82 -0
- package/skills/state-machine.md +126 -0
- package/src/setgame.utils.js +47 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace BgaHarness;
|
|
6
|
+
|
|
7
|
+
use PDO;
|
|
8
|
+
use PDOStatement;
|
|
9
|
+
use PHPUnit\Framework\Assert;
|
|
10
|
+
|
|
11
|
+
class BgaDatabaseFake
|
|
12
|
+
{
|
|
13
|
+
private PDO $pdo;
|
|
14
|
+
|
|
15
|
+
public function __construct()
|
|
16
|
+
{
|
|
17
|
+
$this->pdo = new PDO('sqlite::memory:');
|
|
18
|
+
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public function DbQuery(string $sql): void
|
|
22
|
+
{
|
|
23
|
+
$this->pdo->exec($sql);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public function getCollectionFromDB(string $sql, bool $bUniqueValue = false): array
|
|
27
|
+
{
|
|
28
|
+
$stmt = $this->query($sql);
|
|
29
|
+
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
30
|
+
|
|
31
|
+
if (!$bUniqueValue) {
|
|
32
|
+
return $rows;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$unique = [];
|
|
36
|
+
foreach ($rows as $row) {
|
|
37
|
+
$values = array_values($row);
|
|
38
|
+
if (count($values) >= 2) {
|
|
39
|
+
$unique[$values[0]] = $values[1];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return $unique;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public function getObjectFromDB(string $sql): ?array
|
|
47
|
+
{
|
|
48
|
+
$stmt = $this->query($sql);
|
|
49
|
+
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
50
|
+
|
|
51
|
+
return $row !== false ? $row : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public function getUniqueValueFromDB(string $sql): mixed
|
|
55
|
+
{
|
|
56
|
+
$stmt = $this->query($sql);
|
|
57
|
+
$value = $stmt->fetchColumn();
|
|
58
|
+
|
|
59
|
+
return $value === false ? null : $value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public function getIntFromDB(string $sql): int
|
|
63
|
+
{
|
|
64
|
+
return (int) $this->getUniqueValueFromDB($sql);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public function seedTable(string $table, array $rows): void
|
|
68
|
+
{
|
|
69
|
+
if ($rows === []) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$columns = $this->collectColumns($rows);
|
|
74
|
+
$this->ensureTableHasColumns($table, $columns, $rows);
|
|
75
|
+
|
|
76
|
+
$placeholders = implode(', ', array_fill(0, count($columns), '?'));
|
|
77
|
+
$columnsSql = implode(', ', array_map(static fn (string $c): string => '"' . $c . '"', $columns));
|
|
78
|
+
$sql = 'INSERT INTO "' . $table . '" (' . $columnsSql . ') VALUES (' . $placeholders . ')';
|
|
79
|
+
|
|
80
|
+
$stmt = $this->pdo->prepare($sql);
|
|
81
|
+
foreach ($rows as $row) {
|
|
82
|
+
$values = [];
|
|
83
|
+
foreach ($columns as $column) {
|
|
84
|
+
$values[] = $row[$column] ?? null;
|
|
85
|
+
}
|
|
86
|
+
$stmt->execute($values);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public function assertRowExists(string $table, array $conditions): void
|
|
91
|
+
{
|
|
92
|
+
[$whereSql, $params] = $this->buildWhere($conditions);
|
|
93
|
+
$sql = 'SELECT COUNT(*) FROM "' . $table . '"' . $whereSql;
|
|
94
|
+
|
|
95
|
+
$stmt = $this->pdo->prepare($sql);
|
|
96
|
+
$stmt->execute($params);
|
|
97
|
+
$count = (int) $stmt->fetchColumn();
|
|
98
|
+
|
|
99
|
+
Assert::assertGreaterThan(
|
|
100
|
+
0,
|
|
101
|
+
$count,
|
|
102
|
+
sprintf('Expected row was not found in table "%s" for conditions: %s', $table, json_encode($conditions))
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public function assertRowCount(string $table, int $expected, array $conditions = []): void
|
|
107
|
+
{
|
|
108
|
+
[$whereSql, $params] = $this->buildWhere($conditions);
|
|
109
|
+
$sql = 'SELECT COUNT(*) FROM "' . $table . '"' . $whereSql;
|
|
110
|
+
|
|
111
|
+
$stmt = $this->pdo->prepare($sql);
|
|
112
|
+
$stmt->execute($params);
|
|
113
|
+
$count = (int) $stmt->fetchColumn();
|
|
114
|
+
|
|
115
|
+
Assert::assertSame(
|
|
116
|
+
$expected,
|
|
117
|
+
$count,
|
|
118
|
+
sprintf(
|
|
119
|
+
'Unexpected row count in table "%s" for conditions %s. Expected %d, got %d.',
|
|
120
|
+
$table,
|
|
121
|
+
json_encode($conditions),
|
|
122
|
+
$expected,
|
|
123
|
+
$count
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private function query(string $sql): PDOStatement
|
|
129
|
+
{
|
|
130
|
+
$stmt = $this->pdo->query($sql);
|
|
131
|
+
if ($stmt === false) {
|
|
132
|
+
throw new \RuntimeException('Query failed: ' . $sql);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return $stmt;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private function collectColumns(array $rows): array
|
|
139
|
+
{
|
|
140
|
+
$columns = [];
|
|
141
|
+
foreach ($rows as $row) {
|
|
142
|
+
foreach (array_keys($row) as $column) {
|
|
143
|
+
if (!in_array($column, $columns, true)) {
|
|
144
|
+
$columns[] = $column;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return $columns;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private function ensureTableHasColumns(string $table, array $columns, array $rows): void
|
|
153
|
+
{
|
|
154
|
+
if (!$this->tableExists($table)) {
|
|
155
|
+
$this->createTable($table, $columns, $rows);
|
|
156
|
+
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$existing = $this->listColumns($table);
|
|
161
|
+
foreach ($columns as $column) {
|
|
162
|
+
if (in_array($column, $existing, true)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
$type = $this->inferColumnType($table, $column, $rows);
|
|
166
|
+
$this->pdo->exec('ALTER TABLE "' . $table . '" ADD COLUMN "' . $column . '" ' . $type);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private function createTable(string $table, array $columns, array $rows): void
|
|
171
|
+
{
|
|
172
|
+
$defs = [];
|
|
173
|
+
foreach ($columns as $column) {
|
|
174
|
+
$defs[] = '"' . $column . '" ' . $this->inferColumnType($table, $column, $rows);
|
|
175
|
+
}
|
|
176
|
+
$sql = 'CREATE TABLE IF NOT EXISTS "' . $table . '" (' . implode(', ', $defs) . ')';
|
|
177
|
+
$this->pdo->exec($sql);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private function tableExists(string $table): bool
|
|
181
|
+
{
|
|
182
|
+
$stmt = $this->pdo->prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?");
|
|
183
|
+
$stmt->execute([$table]);
|
|
184
|
+
|
|
185
|
+
return (bool) $stmt->fetchColumn();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private function listColumns(string $table): array
|
|
189
|
+
{
|
|
190
|
+
$stmt = $this->query('PRAGMA table_info("' . $table . '")');
|
|
191
|
+
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
192
|
+
|
|
193
|
+
return array_map(static fn (array $row): string => (string) $row['name'], $rows);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private function inferColumnType(string $table, string $column, array $rows): string
|
|
197
|
+
{
|
|
198
|
+
if ($column === $table . '_id' || str_ends_with($column, '_id')) {
|
|
199
|
+
return 'INTEGER';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
foreach ($rows as $row) {
|
|
203
|
+
if (!array_key_exists($column, $row) || $row[$column] === null) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (is_int($row[$column]) || is_bool($row[$column])) {
|
|
208
|
+
return 'INTEGER';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (is_float($row[$column])) {
|
|
212
|
+
return 'REAL';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return 'TEXT';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private function buildWhere(array $conditions): array
|
|
220
|
+
{
|
|
221
|
+
if ($conditions === []) {
|
|
222
|
+
return ['', []];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
$clauses = [];
|
|
226
|
+
$params = [];
|
|
227
|
+
foreach ($conditions as $column => $value) {
|
|
228
|
+
$clauses[] = '"' . $column . '" = ?';
|
|
229
|
+
$params[] = $value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return [' WHERE ' . implode(' AND ', $clauses), $params];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace BgaHarness;
|
|
6
|
+
|
|
7
|
+
use PHPUnit\Framework\Assert;
|
|
8
|
+
use PHPUnit\Framework\TestCase;
|
|
9
|
+
|
|
10
|
+
abstract class BgaGameTestCase extends TestCase
|
|
11
|
+
{
|
|
12
|
+
protected BgaStubs $game;
|
|
13
|
+
|
|
14
|
+
protected function setUp(): void
|
|
15
|
+
{
|
|
16
|
+
parent::setUp();
|
|
17
|
+
$this->game = $this->createGame();
|
|
18
|
+
$this->game->_setPlayers($this->defaultPlayers());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
abstract protected function createGame(): BgaStubs;
|
|
22
|
+
|
|
23
|
+
protected function givenActivePlayer(int $playerId): static
|
|
24
|
+
{
|
|
25
|
+
$this->game->_setActivePlayer($playerId);
|
|
26
|
+
$this->game->_setCurrentPlayer($playerId);
|
|
27
|
+
|
|
28
|
+
return $this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected function givenCurrentPlayer(int $playerId): static
|
|
32
|
+
{
|
|
33
|
+
$this->game->_setCurrentPlayer($playerId);
|
|
34
|
+
|
|
35
|
+
return $this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected function givenState(string $stateName): static
|
|
39
|
+
{
|
|
40
|
+
$this->game->_setState($stateName);
|
|
41
|
+
|
|
42
|
+
return $this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected function givenDatabaseRows(string $table, array $rows): static
|
|
46
|
+
{
|
|
47
|
+
$this->game->_getDb()->seedTable($table, $rows);
|
|
48
|
+
|
|
49
|
+
return $this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected function givenGameStateValue(string $name, int $value): static
|
|
53
|
+
{
|
|
54
|
+
$this->game->_setGameStateValue($name, $value);
|
|
55
|
+
|
|
56
|
+
return $this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected function whenAction(string $method, array $args = []): ActionResult
|
|
60
|
+
{
|
|
61
|
+
try {
|
|
62
|
+
$result = $this->game->$method(...array_values($args));
|
|
63
|
+
|
|
64
|
+
return ActionResult::success($result);
|
|
65
|
+
} catch (BgaUserException $e) {
|
|
66
|
+
return ActionResult::userError($e->getMessage());
|
|
67
|
+
} catch (BgaVisibleSystemException $e) {
|
|
68
|
+
return ActionResult::systemError($e->getMessage());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected function thenStateShouldBe(string $expected): void
|
|
73
|
+
{
|
|
74
|
+
Assert::assertSame($expected, $this->game->_getState());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected function thenNotificationSent(string $type, array $dataSubset = []): void
|
|
78
|
+
{
|
|
79
|
+
$this->game->_getNotifications()->assertNotified($type, $dataSubset);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected function thenNotificationNotSent(string $type): void
|
|
83
|
+
{
|
|
84
|
+
$this->game->_getNotifications()->assertNotNotified($type);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected function thenPlayerNotifiedWith(int $playerId, string $type, array $dataSubset = []): void
|
|
88
|
+
{
|
|
89
|
+
$this->game->_getNotifications()->assertNotifiedPlayer($playerId, $type, $dataSubset);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected function thenDatabaseHas(string $table, array $conditions): void
|
|
93
|
+
{
|
|
94
|
+
$this->game->_getDb()->assertRowExists($table, $conditions);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected function thenDatabaseCount(string $table, int $count, array $conditions = []): void
|
|
98
|
+
{
|
|
99
|
+
$this->game->_getDb()->assertRowCount($table, $count, $conditions);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected function defaultPlayers(): array
|
|
103
|
+
{
|
|
104
|
+
return [
|
|
105
|
+
1 => ['player_id' => 1, 'player_name' => 'Alice', 'player_score' => 0],
|
|
106
|
+
2 => ['player_id' => 2, 'player_name' => 'Bob', 'player_score' => 0],
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class ActionResult
|
|
112
|
+
{
|
|
113
|
+
public bool $succeeded = false;
|
|
114
|
+
public ?string $errorCode = null;
|
|
115
|
+
public mixed $returnValue = null;
|
|
116
|
+
|
|
117
|
+
public static function success(mixed $value): self
|
|
118
|
+
{
|
|
119
|
+
$result = new self();
|
|
120
|
+
$result->succeeded = true;
|
|
121
|
+
$result->errorCode = null;
|
|
122
|
+
$result->returnValue = $value;
|
|
123
|
+
|
|
124
|
+
return $result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public static function userError(string $code): self
|
|
128
|
+
{
|
|
129
|
+
$result = new self();
|
|
130
|
+
$result->succeeded = false;
|
|
131
|
+
$result->errorCode = $code;
|
|
132
|
+
$result->returnValue = null;
|
|
133
|
+
|
|
134
|
+
return $result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public static function systemError(string $message): self
|
|
138
|
+
{
|
|
139
|
+
$result = new self();
|
|
140
|
+
$result->succeeded = false;
|
|
141
|
+
$result->errorCode = $message;
|
|
142
|
+
|
|
143
|
+
return $result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public function assertSucceeded(): void
|
|
147
|
+
{
|
|
148
|
+
Assert::assertTrue($this->succeeded, 'Expected action to succeed.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public function assertFailedWith(string $errorCode): void
|
|
152
|
+
{
|
|
153
|
+
Assert::assertFalse($this->succeeded, 'Expected action to fail.');
|
|
154
|
+
Assert::assertSame(
|
|
155
|
+
$errorCode,
|
|
156
|
+
$this->errorCode,
|
|
157
|
+
sprintf('Expected failure code "%s", got "%s".', $errorCode, (string) $this->errorCode)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace BgaHarness;
|
|
6
|
+
|
|
7
|
+
use PHPUnit\Framework\Assert;
|
|
8
|
+
|
|
9
|
+
class BgaNotificationSpy
|
|
10
|
+
{
|
|
11
|
+
private array $notifications = [];
|
|
12
|
+
|
|
13
|
+
public function notifyAllPlayers(string $type, string $message, array $data): void
|
|
14
|
+
{
|
|
15
|
+
$this->notifications[] = [
|
|
16
|
+
'target' => 'all',
|
|
17
|
+
'type' => $type,
|
|
18
|
+
'message' => $message,
|
|
19
|
+
'data' => $data,
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public function notifyPlayer(int $playerId, string $type, string $message, array $data): void
|
|
24
|
+
{
|
|
25
|
+
$this->notifications[] = [
|
|
26
|
+
'target' => $playerId,
|
|
27
|
+
'type' => $type,
|
|
28
|
+
'message' => $message,
|
|
29
|
+
'data' => $data,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public function assertNotified(string $type, array $dataSubset = []): void
|
|
34
|
+
{
|
|
35
|
+
foreach ($this->notifications as $notification) {
|
|
36
|
+
if ($notification['type'] !== $type) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if ($this->containsSubset($notification['data'], $dataSubset)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Assert::fail(
|
|
45
|
+
sprintf(
|
|
46
|
+
'Expected notification "%s" with subset %s was not sent. Captured notifications: %s',
|
|
47
|
+
$type,
|
|
48
|
+
json_encode($dataSubset),
|
|
49
|
+
json_encode($this->notifications)
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public function assertNotifiedPlayer(int $playerId, string $type, array $dataSubset = []): void
|
|
55
|
+
{
|
|
56
|
+
foreach ($this->notifications as $notification) {
|
|
57
|
+
if ($notification['target'] !== $playerId || $notification['type'] !== $type) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if ($this->containsSubset($notification['data'], $dataSubset)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Assert::fail(
|
|
66
|
+
sprintf(
|
|
67
|
+
'Expected player %d notification "%s" with subset %s was not sent. Captured notifications: %s',
|
|
68
|
+
$playerId,
|
|
69
|
+
$type,
|
|
70
|
+
json_encode($dataSubset),
|
|
71
|
+
json_encode($this->notifications)
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public function assertNotNotified(string $type): void
|
|
77
|
+
{
|
|
78
|
+
foreach ($this->notifications as $notification) {
|
|
79
|
+
if ($notification['type'] === $type) {
|
|
80
|
+
Assert::fail(
|
|
81
|
+
sprintf(
|
|
82
|
+
'Notification "%s" should not have been sent, but was captured: %s',
|
|
83
|
+
$type,
|
|
84
|
+
json_encode($notification)
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public function assertNotificationCount(int $expected): void
|
|
92
|
+
{
|
|
93
|
+
Assert::assertCount($expected, $this->notifications);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public function getNotifications(): array
|
|
97
|
+
{
|
|
98
|
+
return $this->notifications;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public function reset(): void
|
|
102
|
+
{
|
|
103
|
+
$this->notifications = [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private function containsSubset(array $actual, array $subset): bool
|
|
107
|
+
{
|
|
108
|
+
foreach ($subset as $key => $value) {
|
|
109
|
+
if (!array_key_exists($key, $actual) || $actual[$key] !== $value) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|