Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

랜덤 스폰 위치 구현 #78

Merged
merged 8 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 125 additions & 18 deletions board/data/handler/internal/board.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
from board.data import Point, Section, Tile, Tiles


Expand All @@ -18,39 +19,52 @@ class BoardHandler:
# sections[y][x]
sections: dict[int, dict[int, Section]] = init_first_section()

# 맵의 각 끝단 섹션 위치
max_x: int = 0
min_x: int = 0
max_y: int = 0
min_y: int = 0

@staticmethod
def fetch(start: Point, end: Point) -> Tiles:
# 반환할 데이터 공간 미리 할당
out_width, out_height = (end.x - start.x + 1), (start.y - end.y + 1)
out = bytearray(out_width * out_height)

# TODO: 새로운 섹션과의 관계로 경계값이 바뀔 수 있음.
# 이를 fetch 결과에 적용시킬 수 있도록 미리 다 만들어놓고 fetch를 시작해야 함.
# 현재는 섹션이 메모리 내부 레퍼런스로 저장되기 때문에 이렇게 미리 받아놓고 할 수 있음.
# 나중에는 다시 섹션을 가져와야 함.
sections = []
for sec_y in range(start.y // Section.LENGTH, end.y // Section.LENGTH - 1, - 1):
for sec_x in range(start.x // Section.LENGTH, end.x // Section.LENGTH + 1):
section = BoardHandler._get_or_create_section(sec_x, sec_y)
sections.append(section)

inner_start = Point(
x=max(start.x, section.abs_x) - (section.abs_x),
y=min(start.y, section.abs_y + Section.LENGTH-1) - section.abs_y
)
inner_end = Point(
x=min(end.x, section.abs_x + Section.LENGTH-1) - section.abs_x,
y=max(end.y, section.abs_y) - section.abs_y
)
for section in sections:
inner_start = Point(
x=max(start.x, section.abs_x) - (section.abs_x),
y=min(start.y, section.abs_y + Section.LENGTH-1) - section.abs_y
)
inner_end = Point(
x=min(end.x, section.abs_x + Section.LENGTH-1) - section.abs_x,
y=max(end.y, section.abs_y) - section.abs_y
)

fetched = section.fetch(start=inner_start, end=inner_end)
fetched = section.fetch(start=inner_start, end=inner_end)

x_gap, y_gap = (inner_end.x - inner_start.x + 1), (inner_start.y - inner_end.y + 1)
x_gap, y_gap = (inner_end.x - inner_start.x + 1), (inner_start.y - inner_end.y + 1)

# start로부터 떨어진 거리
out_x = (section.abs_x + inner_start.x) - start.x
out_y = start.y - (section.abs_y + inner_start.y)
# start로부터 떨어진 거리
out_x = (section.abs_x + inner_start.x) - start.x
out_y = start.y - (section.abs_y + inner_start.y)

for row_num in range(y_gap):
out_idx = (out_width * (out_y + row_num)) + out_x
data_idx = row_num * x_gap
for row_num in range(y_gap):
out_idx = (out_width * (out_y + row_num)) + out_x
data_idx = row_num * x_gap

data = fetched.data[data_idx:data_idx+x_gap]
out[out_idx:out_idx+x_gap] = data
data = fetched.data[data_idx:data_idx+x_gap]
out[out_idx:out_idx+x_gap] = data

return Tiles(data=out)
onee-only marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -71,12 +85,57 @@ def update_tile(p: Point, tile: Tile):
# 지금은 안 해도 되긴 할텐데 일단 해 놓기
BoardHandler.sections[sec_p.y][sec_p.x] = section

@staticmethod
def get_random_open_position() -> Point:
"""
전체 맵에서 랜덤한 열린 타일 위치를 하나 찾는다.
섹션이 하나 이상 존재해야한다.
"""
# 이미 방문한 섹션들
visited = set()

sec_x_range = (BoardHandler.min_x, BoardHandler.max_x)
sec_y_range = (BoardHandler.min_y, BoardHandler.max_y)

while True:
rand_p = Point(
x=random.randint(sec_x_range[0], sec_x_range[1]),
y=random.randint(sec_y_range[0], sec_y_range[1])
)

if (rand_p.x, rand_p.y) in visited:
continue

visited.add((rand_p.x, rand_p.y))

chosen_section = BoardHandler._get_section_or_none(rand_p.x, rand_p.y)
if chosen_section is None:
continue

# 섹션 내부의 랜덤한 열린 타일 위치를 찾는다.
inner_point = randomly_find_open_tile(chosen_section)
if inner_point is None:
continue

open_point = Point(
x=chosen_section.abs_x + inner_point.x,
y=chosen_section.abs_y + inner_point.y
)

return open_point

@staticmethod
def _get_or_create_section(x: int, y: int) -> Section:
if y not in BoardHandler.sections:
BoardHandler.max_y = max(BoardHandler.max_y, y)
BoardHandler.min_y = min(BoardHandler.min_y, y)

BoardHandler.sections[y] = {}

if x not in BoardHandler.sections[y]:
BoardHandler.max_x = max(BoardHandler.max_x, x)
BoardHandler.min_x = min(BoardHandler.min_x, x)

new_section = Section.create(Point(x, y))

# (x, y)
Expand Down Expand Up @@ -110,3 +169,51 @@ def _get_or_create_section(x: int, y: int) -> Section:
def _get_section_or_none(x: int, y: int) -> Section | None:
if y in BoardHandler.sections and x in BoardHandler.sections[y]:
return BoardHandler.sections[y][x]


def randomly_find_open_tile(section: Section) -> Point | None:
"""
섹션 안에서 랜덤한 열린 타일 위치를 찾는다.
시작 위치, 순회 방향의 순서를 무작위로 잡아 탐색한다.
만약 열린 타일이 존재하지 않는다면 None.
"""

# (증감값, 한계값)
directions = [
(1, Section.LENGTH - 1), (-1, 0) # 순방향, 역방향
]
random.shuffle(directions)

x_start = random.randint(0, Section.LENGTH - 1)
y_start = random.randint(0, Section.LENGTH - 1)

pointers = [0, 0] # first, second
start_values = [0, 0]

x_first = random.choice([True, False])
x_pointer = 0 if x_first else 1
y_pointer = 1 if x_first else 0

start_values[x_pointer] = x_start
start_values[y_pointer] = y_start

# second 양방향 탐색
for num, limit in directions:
for second in range(start_values[1], limit + num, num):
pointers[1] = second

# first 양방향 탐색
for num, limit in directions:
for first in range(start_values[0], limit + num, num):
pointers[0] = first

x = pointers[x_pointer]
y = pointers[y_pointer]

idx = y * Section.LENGTH + x

tile = Tile.from_int(section.data[idx])
if tile.is_open:
# 좌표계에 맞게 y 반전
y = Section.LENGTH - y - 1
return Point(x, y)
21 changes: 21 additions & 0 deletions board/data/handler/test/board_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ def test_update_tile(self):

self.assertEqual(tiles.data[0], tile.data)

def test_get_random_open_position(self):
for _ in range(10):
point = BoardHandler.get_random_open_position()

tiles = BoardHandler.fetch(point, point)
tile = Tile.from_int(tiles.data[0])

self.assertTrue(tile.is_open)

def test_get_random_open_position_one_section_one_open(self):
sec = BoardHandler.sections[-1][0]
BoardHandler.sections = {-1: {0: sec}}

for _ in range(10):
point = BoardHandler.get_random_open_position()

tiles = BoardHandler.fetch(point, point)
tile = Tile.from_int(tiles.data[0])

self.assertTrue(tile.is_open)


if __name__ == "__main__":
unittest.main()
4 changes: 4 additions & 0 deletions board/data/handler/test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ def setup_board():
-1: Section(Point(-1, -1), tile_state_3)
}
}
BoardHandler.max_x = 0
BoardHandler.min_x = -1
BoardHandler.max_y = 0
BoardHandler.min_y = -1
30 changes: 26 additions & 4 deletions board/event/handler/internal/board_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
TilesEvent,
NewConnEvent,
NewConnPayload,
NewCursorCandidatePayload,
TryPointingPayload,
PointingResultPayload,
PointEvent,
Expand All @@ -35,14 +36,35 @@ async def receive_fetch_tiles(message: Message[FetchTilesPayload]):
async def receive_new_conn(message: Message[NewConnPayload]):
sender = message.payload.conn_id

# 0, 0 기준으로 fetch
width = message.payload.width
height = message.payload.height

start_p = Point(x=-width, y=height)
end_p = Point(x=width, y=-height)
# 커서의 위치
position = BoardHandler.get_random_open_position()

await BoardEventHandler._publish_tiles(start_p, end_p, [sender])
start_p = Point(
x=position.x - width,
y=position.y + height
)
end_p = Point(
x=position.x+width,
y=position.y-height
)
publish_tiles = BoardEventHandler._publish_tiles(start_p, end_p, [sender])

message = Message(
event=NewConnEvent.NEW_CURSOR_CANDIDATE,
payload=NewCursorCandidatePayload(
conn_id=message.payload.conn_id,
width=width, height=height,
position=position
)
)

await asyncio.gather(
publish_tiles,
EventBroker.publish(message)
)

@staticmethod
async def _publish_tiles(start: Point, end: Point, to: list[str]):
Expand Down
48 changes: 29 additions & 19 deletions board/event/handler/test/board_handler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
TilesPayload,
NewConnEvent,
NewConnPayload,
NewCursorCandidatePayload,
TryPointingPayload,
PointingResultPayload,
PointEvent,
Expand Down Expand Up @@ -125,41 +126,50 @@ async def test_fetch_tiles_receiver_normal_case(self, mock: AsyncMock):
@patch("event.EventBroker.publish")
async def test_receive_new_conn(self, mock: AsyncMock):
conn_id = "ayo"
width = 1
height = 1
message = Message(
event=NewConnEvent.NEW_CONN,
payload=NewConnPayload(conn_id=conn_id, width=1, height=1)
payload=NewConnPayload(conn_id=conn_id, width=width, height=height)
)

await BoardEventHandler.receive_new_conn(message)

mock.assert_called_once()
got: Message[TilesPayload] = mock.mock_calls[0].args[0]
# tiles, new-cursor-candidate
self.assertEqual(len(mock.mock_calls), 2)

# new-cursor-candidate
got: Message[NewCursorCandidatePayload] = mock.mock_calls[0].args[0]
self.assertEqual(type(got), Message)
self.assertEqual(got.event, "multicast")
self.assertEqual(got.event, NewConnEvent.NEW_CURSOR_CANDIDATE)

self.assertEqual(type(got.payload), NewCursorCandidatePayload)
self.assertEqual(got.payload.conn_id, conn_id)
self.assertEqual(got.payload.width, width)
self.assertEqual(got.payload.height, height)

position = got.payload.position
tiles = BoardHandler.fetch(position, position)
tile = Tile.from_int(tiles.data[0])
self.assertTrue(tile.is_open)

# tiles
got: Message[TilesPayload] = mock.mock_calls[1].args[0]
self.assertEqual(type(got), Message)
self.assertEqual(got.event, "multicast")
self.assertIn("target_conns", got.header)
self.assertEqual(len(got.header["target_conns"]), 1)
self.assertEqual(got.header["target_conns"][0], conn_id)
self.assertIn("origin_event", got.header)
self.assertEqual(got.header["origin_event"], TilesEvent.TILES)

self.assertEqual(type(got.payload), TilesPayload)
self.assertEqual(got.payload.start_p.x, -1)
self.assertEqual(got.payload.start_p.y, 1)
self.assertEqual(got.payload.end_p.x, 1)
self.assertEqual(got.payload.end_p.y, -1)
self.assertEqual(got.payload.start_p, Point(position.x-width, position.y+height))
self.assertEqual(got.payload.end_p, Point(position.x+width, position.y-height))

# 하는 김에 마스킹까지 같이 테스트
empty_open = Tile.from_int(0b10000000)
one_open = Tile.from_int(0b10000001)
closed = Tile.from_int(0b00000000)
blue_flag = Tile.from_int(0b00110000)
purple_flag = Tile.from_int(0b00111000)
expected = BoardHandler.fetch(got.payload.start_p, got.payload.end_p)
expected.hide_info()

expected = Tiles(data=bytearray([
one_open.data, one_open.data, blue_flag.data,
empty_open.data, one_open.data, closed.data,
one_open.data, one_open.data, purple_flag.data
]))
self.assertEqual(got.payload.tiles, expected.to_str())


Expand Down
4 changes: 3 additions & 1 deletion cursor/data/handler/internal/cursor_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ class CursorHandler:
watching: dict[str, list[str]] = {}

@staticmethod
def create_cursor(conn_id: str):
def create_cursor(conn_id: str, position: Point, width: int, height: int):
cursor = Cursor.create(conn_id)
cursor.position = position
cursor.set_size(width=width, height=height)

CursorHandler.cursor_dict[conn_id] = cursor

Expand Down
8 changes: 7 additions & 1 deletion cursor/data/handler/test/cursor_handler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ def tearDown(self):

def test_create(self):
conn_id = "example_conn_id"
_ = CursorHandler.create_cursor(conn_id)
width, height = 10, 10
position = Point(1, 1)

_ = CursorHandler.create_cursor(conn_id, position, width, height)

self.assertIn(conn_id, CursorHandler.cursor_dict)
self.assertEqual(type(CursorHandler.cursor_dict[conn_id]), Cursor)
self.assertEqual(CursorHandler.cursor_dict[conn_id].conn_id, conn_id)
self.assertEqual(CursorHandler.cursor_dict[conn_id].width, width)
self.assertEqual(CursorHandler.cursor_dict[conn_id].height, height)
self.assertEqual(CursorHandler.cursor_dict[conn_id].position, position)

def test_get_cursor(self):
a_cur: Cursor | None = CursorHandler.get_cursor("A")
Expand Down
Loading
Loading