Skip to content

Commit 0c066ff

Browse files
Put in the core infrastructure for loading boards.
1 parent 1c65834 commit 0c066ff

File tree

8 files changed

+244
-4
lines changed

8 files changed

+244
-4
lines changed

pacai/boards/medium-classic.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
%%%%%%%%%%%%%%%%%%%%
2+
%o...%........%....%
3+
%.%%.%.%%%%%%.%.%%.%
4+
%.%..............%.%
5+
%.%.%%.%% %%.%%.%.%
6+
%......%G G%......%
7+
%.%.%%.%%%%%%.%%.%.%
8+
%.%..............%.%
9+
%.%%.%.%%%%%%.%.%%.%
10+
%....%...P....%...o%
11+
%%%%%%%%%%%%%%%%%%%%

pacai/core/board.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,65 @@
1+
import os
2+
import re
3+
4+
import pacai.util.file
5+
import pacai.util.json
6+
import pacai.util.reflection
7+
8+
THIS_DIR: str = os.path.join(os.path.dirname(os.path.realpath(__file__)))
9+
BOARDS_DIR: str = os.path.join(THIS_DIR, '..', 'boards')
10+
11+
SEPARATOR_PATTERN: re.Pattern = re.compile(r'^\s*-{3,}\s*$')
12+
13+
DEFAULT_BOARD_CLASS = 'pacai.core.board.Board'
14+
115
class Board:
216
"""
317
A board represents the static (non-agent) components of a game.
4-
For example, a layout contains the walls and collectable items.
18+
For example, a board contains the walls and collectable items.
19+
20+
Most types of games (anything that would subclass pacai.core.game.Game) should probably
21+
also subclass this to make their own type of board.
22+
23+
On disk, boards are represented in files that have two sections (divided by a '---' line).
24+
The first section is a JSON object that holds any options for the board.
25+
The second section is a textual representation of the board.
26+
The specific board class (usually specified by the board options) should know how to interpret the text-based board.
527
"""
628

7-
# TEST
8-
pass
29+
def __init__(self, marker_wall = '%', **kwargs) -> None:
30+
# TEST
31+
pass
32+
33+
def load_path(path: str) -> Board:
34+
""" Load a board from a file. """
35+
36+
text = pacai.util.file.read(path, strip = False)
37+
return load_string(text)
38+
39+
def load_string(text: str) -> Board:
40+
""" Load a board from a string. """
41+
42+
separator_index = -1
43+
lines = text.split("\n")
44+
45+
for i in range(len(lines)):
46+
if (SEPARATOR_PATTERN.match(lines[i])):
47+
separator_index = i
48+
break
49+
50+
if (separator_index == -1):
51+
# No separator was found.
52+
options_text = ''
53+
board_text = "\n".join(lines)
54+
else:
55+
options_text = "\n".join(lines[:i])
56+
board_text = "\n".join(lines[(i + 1):])
57+
58+
options_text = options_text.strip()
59+
if (len(options_text) == 0):
60+
options = {}
61+
else:
62+
options = pacai.util.json.loads(options_text)
63+
64+
board_class = options.get('class', DEFAULT_BOARD_CLASS)
65+
return pacai.util.reflection.new_object(board_class, **options)

pacai/core/test_agent.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,3 @@ def test_ordering_base(self):
1919
(lower_ticket, higher_ticket) = test_cases[i]
2020
with self.subTest(msg = f"Case {i}: {lower_ticket} < {higher_ticket}"):
2121
self.assertTrue((lower_ticket < higher_ticket))
22-

pacai/core/test_board.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import glob
2+
import os
3+
4+
import pacai.core.board
5+
import pacai.test.base
6+
7+
class BoardTest(pacai.test.base.BaseTest):
8+
# Load all the known/included boards.
9+
def test_load_default_boards(self):
10+
for path in glob.glob(os.path.join(pacai.core.board.BOARDS_DIR, '*.txt')):
11+
pacai.core.board.load_path(path)
12+
13+
def test_load_test_boards(self):
14+
# [(board, expected error substring), ...]
15+
test_cases = [
16+
('', None),
17+
(TEST_BOARD_NO_SEP, None),
18+
(TEST_BOARD_OPTIONS, None),
19+
(TEST_BOARD_SEP_EMPTY_OPTIONS, None),
20+
(TEST_BOARD_EMPTY_BOARD, None),
21+
(TEST_BOARD_FULL_EMPTY, None),
22+
(TEST_BOARD_FULL_EMPTY_SEP, None),
23+
24+
(TEST_BOARD_ERROR_BAD_CLASS, "Cannot find class 'ZZZ' in module 'pacai.core.board'."),
25+
]
26+
27+
for i in range(len(test_cases)):
28+
(text_board, error_substring) = test_cases[i]
29+
with self.subTest(msg = f"Case {i}:"):
30+
try:
31+
pacai.core.board.load_string(text_board)
32+
except Exception as ex:
33+
if (error_substring is None):
34+
self.fail(f"Unexpected error: '{str(ex)}'.")
35+
36+
self.assertIn(error_substring, str(ex), 'Error is not as expected.')
37+
continue
38+
39+
if (error_substring is not None):
40+
self.fail(f"Did not get expected error: '{error_substring}'.")
41+
42+
TEST_BOARD_NO_SEP = '''
43+
%%%%%
44+
%. P%
45+
%%%%%
46+
'''
47+
48+
TEST_BOARD_OPTIONS = '''
49+
{"marker_wall": "#"}
50+
---
51+
#####
52+
#. P#
53+
#####
54+
'''
55+
56+
TEST_BOARD_SEP_EMPTY_OPTIONS = '''
57+
---
58+
%%%%%
59+
%. P%
60+
%%%%%
61+
'''
62+
63+
TEST_BOARD_EMPTY_BOARD = '''
64+
{"marker_wall": "#"}
65+
---
66+
'''
67+
68+
TEST_BOARD_FULL_EMPTY = '''
69+
'''
70+
71+
TEST_BOARD_FULL_EMPTY_SEP = '''
72+
---
73+
'''
74+
75+
TEST_BOARD_ERROR_BAD_CLASS = '''
76+
{"class": "pacai.core.board.ZZZ"}
77+
---
78+
%%%%%
79+
%. P%
80+
%%%%%
81+
'''

pacai/util/file.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
def read(path, strip = True):
2+
with open(path, 'r') as file:
3+
contents = file.read()
4+
5+
if (strip):
6+
contents = contents.strip()
7+
8+
return contents
9+
10+
def write(path, contents, strip = True, newline = True):
11+
if (contents is None):
12+
contents = ''
13+
14+
if (strip):
15+
contents = contents.strip()
16+
17+
if (newline):
18+
contents += "\n"
19+
20+
with open(path, 'w') as file:
21+
file.write(contents)

pacai/util/json.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
This file standardizes how we write and read JSON files.
3+
Specifically, we try to be flexible when reading (using JSON5),
4+
and strict when writing (using vanilla JSON).
5+
"""
6+
7+
import json
8+
9+
import json5
10+
11+
def load(file_obj, **kwargs):
12+
return json5.load(file_obj, **kwargs)
13+
14+
def loads(text, **kwargs):
15+
return json5.loads(text, **kwargs)
16+
17+
def load_path(path, **kwargs):
18+
try:
19+
with open(path, 'r') as file:
20+
return load(file, **kwargs)
21+
except Exception as ex:
22+
raise ValueError(f"Failed to read JSON file '{path}'.") from ex
23+
24+
def dump(data, file_obj, **kwargs):
25+
return json.dump(data, file_obj, **kwargs)
26+
27+
def dumps(data, **kwargs):
28+
return json.dumps(data, **kwargs)
29+
30+
def dump_path(data, path, **kwargs):
31+
with open(path, 'w') as file:
32+
dump(data, file, **kwargs)

pacai/util/reflection.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
Reflection is the ability for a program to examine and modify its own code and structure when running.
3+
For example, you may want to find the type of a variable or create an object without knowing its type when you write the code.
4+
See: https://en.wikipedia.org/wiki/Reflective_programming .
5+
6+
This file aims to contain all the reflection necessary for this project
7+
(as it can be confusing for students).
8+
"""
9+
10+
import importlib
11+
import typing
12+
13+
def new_object(class_name: str, *args, **kwargs) -> typing.Any:
14+
"""
15+
Create a new instance of the specified class,
16+
passing along the args and kwargs.
17+
The class name should be fully qualified, e.g., 'pacai.core.agent.Agent', not just 'Agent'.
18+
19+
The module must be importable (i.e., already in the PATH).
20+
"""
21+
22+
parts = class_name.split('.')
23+
module_name = '.'.join(parts[0:-1])
24+
target_name = parts[-1]
25+
26+
if (len(parts) == 1):
27+
raise ValueError(f"Non-qualified name supplied '{class_name}'.")
28+
29+
try:
30+
module = importlib.import_module(module_name)
31+
except ImportError:
32+
raise ValueError(f"Unable to locate module '{module_name}'.")
33+
34+
target_class = getattr(module, target_name, None)
35+
if (target_class is None):
36+
raise ValueError(f"Cannot find class '{target_name}' in module '{module_name}'.")
37+
38+
return target_class(*args, **kwargs)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
Pillow>=8.3.2
2+
json5>=0.9.14

0 commit comments

Comments
 (0)