Skip to content

Commit

Permalink
Implement message log UI
Browse files Browse the repository at this point in the history
Display only unread messages in the main UI, hiding them once an action
is entered. Add a continuation prompt when there are more unread lines
than the UI can display.
  • Loading branch information
leomartius committed Sep 13, 2024
1 parent fbc4e91 commit 5657c43
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 19 deletions.
29 changes: 18 additions & 11 deletions game/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@


class MessageLog:
def __init__(self, max_len: int = 22):
self.messages: deque[str] = deque(maxlen=max_len)

def __len__(self) -> int:
return len(self.messages)
def __init__(self, max_size: int = 25):
self._messages: deque[str] = deque(maxlen=max_size)
self._unread = 0

def append(self, message: str) -> None:
self.messages.append(message)
self._messages.append(message)
self._unread = min(self._unread + 1, len(self._messages))

def get_latest(self, n: int) -> list[str]:
return list(self._messages)[-n:]

@property
def unread(self) -> int:
return self._unread

def get(self, n: int = 1) -> list[str]:
selected = []
for i in range(max(len(self.messages) - n, 0), len(self.messages)):
selected.append(self.messages[i])
return selected
def get_unread(self, n: int) -> list[str]:
if self._unread == 0:
return []
unread_messages = list(self._messages)[-self._unread :]
self._unread = max(self._unread - n, 0)
return unread_messages[:n]
2 changes: 1 addition & 1 deletion game/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SaveFile:
signature: str


SIGNATURE = 'YARC:0.1.0'
SIGNATURE = 'YARC:0.2.0'


def save_game(filename: Path, player: Player, level: Level, log: MessageLog) -> None:
Expand Down
55 changes: 48 additions & 7 deletions game/state.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import annotations # noqa: I001
from __future__ import annotations

import tcod

Expand All @@ -9,8 +9,18 @@
from game.input import Command, MoveCommand, handle_play_event, is_cancel, is_continue, to_index
from game.level import Level
from game.messages import MessageLog
from game.render import (fullscreen_cancel_prompt, fullscreen_wait_prompt, highlight_cursor, map_height, message_lines,
render_inventory, render_map, render_messages, render_status)
from game.render import (
fullscreen_cancel_prompt,
fullscreen_wait_prompt,
highlight_cursor,
map_height,
message_lines,
render_inventory,
render_map,
render_messages,
render_status,
screen_height,
)
from game.theme import Theme
from game.version import version_string

Expand All @@ -24,10 +34,15 @@ def event(self, event: tcod.event.Event, player: Player, level: Level, log: Mess


class Play(State):
def __init__(self) -> None:
self.messages: list[str] | None = None

def render(self, console: tcod.Console, player: Player, level: Level, log: MessageLog, theme: Theme) -> None:
if self.messages is None:
self.messages = log.get_unread(message_lines)
render_messages(console, self.messages, 0, theme)
render_map(console, level, message_lines, theme)
render_status(console, player, level, message_lines + map_height, theme)
render_messages(console, log.get(message_lines), 0, theme)

def event(self, event: tcod.event.Event, player: Player, level: Level, log: MessageLog) -> State:
command = handle_play_event(event)
Expand Down Expand Up @@ -65,6 +80,7 @@ def event(self, event: tcod.event.Event, player: Player, level: Level, log: Mess
return do_action(action, player, level, log)
case Command.VERSION:
log.append(f"Y.A.R.C. version {version_string.lstrip('v')}")
return Play()
return self


Expand All @@ -77,21 +93,45 @@ def do_action(action: Action, player: Player, level: Level, log: MessageLog) ->
actor.ai.take_turn(actor, level, player).perform(actor, level, log)
if player.stats.hp == 0:
return GameOver()
if log.unread > message_lines:
return More()
return Play()


class More(State):
def __init__(self) -> None:
self.messages: list[str] | None = None

def render(self, console: tcod.Console, player: Player, level: Level, log: MessageLog, theme: Theme) -> None:
if self.messages is None:
self.messages = log.get_unread(message_lines)
self.messages[-1] += "--More--"
render_messages(console, self.messages, 0, theme)
render_map(console, level, message_lines, theme)
render_status(console, player, level, message_lines + map_height, theme)

def event(self, event: tcod.event.Event, player: Player, level: Level, log: MessageLog) -> State:
if is_continue(event):
if log.unread > message_lines:
return More()
else:
return Play()
else:
return self


class GameOver(State):
def render(self, console: tcod.Console, player: Player, level: Level, log: MessageLog, theme: Theme) -> None:
render_status(console, player, level, message_lines + map_height, theme)
render_messages(console, log.get(message_lines), 0, theme)
render_messages(console, log.get_latest(message_lines), 0, theme)

def event(self, event: tcod.event.Event, player: Player, level: Level, log: MessageLog) -> State:
return self


class FullscreenLog(State):
def render(self, console: tcod.Console, player: Player, level: Level, log: MessageLog, theme: Theme) -> None:
render_messages(console, log.get(len(log)), 0, theme)
render_messages(console, log.get_latest(screen_height - 2), 0, theme)
fullscreen_wait_prompt(console, theme)

def event(self, event: tcod.event.Event, player: Player, level: Level, log: MessageLog) -> State:
Expand All @@ -108,7 +148,7 @@ def __init__(self, player: Player, log: MessageLog):
def render(self, console: tcod.Console, player: Player, level: Level, log: MessageLog, theme: Theme) -> None:
render_map(console, level, message_lines, theme)
highlight_cursor(console, self.x, self.y, message_lines)
render_messages(console, log.get(message_lines), 0, theme)
render_messages(console, log.get_latest(message_lines), 0, theme)
fullscreen_wait_prompt(console, theme)

def event(self, event: tcod.event.Event, player: Player, level: Level, log: MessageLog) -> State:
Expand All @@ -120,6 +160,7 @@ def event(self, event: tcod.event.Event, player: Player, level: Level, log: Mess
item = level.get_item_at(self.x, self.y)
if item:
log.append(item.name)
log.get_unread(1)
return Play()
command = handle_play_event(event)
if isinstance(command, MoveCommand):
Expand Down
121 changes: 121 additions & 0 deletions tests/test_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import pytest

import game.messages

MAX_LEN = 10
MESSAGE = 'a message'
ALL = MAX_LEN + 1


@pytest.fixture
def empty_log():
return game.messages.MessageLog(max_size=MAX_LEN)


@pytest.fixture
def half_log(empty_log):
for i in range(MAX_LEN // 2):
empty_log.append(f'{i + 1}')
return empty_log


@pytest.fixture
def full_log(empty_log):
for i in range(MAX_LEN):
empty_log.append(f'{i + 1}')
return empty_log


@pytest.fixture
def read_log(full_log):
full_log.get_unread(ALL)
return full_log


def test_append_empty(empty_log):
empty_log.append(MESSAGE)
assert empty_log.unread == 1
assert empty_log.get_latest(ALL) == [MESSAGE]
assert empty_log.get_unread(ALL) == [MESSAGE]


def test_append_half(half_log):
half_log.append(MESSAGE)
assert half_log.unread == MAX_LEN // 2 + 1
assert half_log.get_latest(ALL) == [f'{i}' for i in range(1, MAX_LEN // 2 + 1)] + [MESSAGE]
assert half_log.get_unread(ALL) == [f'{i}' for i in range(1, MAX_LEN // 2 + 1)] + [MESSAGE]


def test_append_full(full_log):
full_log.append(MESSAGE)
assert full_log.unread == MAX_LEN
assert full_log.get_latest(ALL) == [f'{i}' for i in range(2, MAX_LEN + 1)] + [MESSAGE]
assert full_log.get_unread(ALL) == [f'{i}' for i in range(2, MAX_LEN + 1)] + [MESSAGE]


def test_append_read(read_log):
read_log.append(MESSAGE)
assert read_log.unread == 1
assert read_log.get_latest(ALL) == [f'{i}' for i in range(2, MAX_LEN + 1)] + [MESSAGE]
assert read_log.get_unread(ALL) == [MESSAGE]


def test_get_latest_empty(empty_log):
assert empty_log.get_latest(1) == []
assert empty_log.get_latest(2) == []
assert empty_log.get_latest(MAX_LEN) == []


def test_get_latest_half(half_log):
assert half_log.get_latest(1) == [f'{MAX_LEN // 2}']
assert half_log.get_latest(2) == [f'{MAX_LEN // 2 - 1}', f'{MAX_LEN // 2}']
assert half_log.get_latest(MAX_LEN // 2) == [f'{i}' for i in range(1, MAX_LEN // 2 + 1)]
assert half_log.get_latest(MAX_LEN // 2 + 1) == [f'{i}' for i in range(1, MAX_LEN // 2 + 1)]


def test_get_latest_full(full_log):
assert full_log.get_latest(1) == [f'{MAX_LEN}']
assert full_log.get_latest(2) == [f'{MAX_LEN - 1}', f'{MAX_LEN}']
assert full_log.get_latest(MAX_LEN // 2) == [f'{i}' for i in range(MAX_LEN // 2 + 1, MAX_LEN + 1)]
assert full_log.get_latest(MAX_LEN) == [f'{i}' for i in range(1, MAX_LEN + 1)]
assert full_log.get_latest(MAX_LEN + 1) == [f'{i}' for i in range(1, MAX_LEN + 1)]


def test_unread_empty(empty_log):
assert empty_log.unread == 0


def test_unread_half(half_log):
assert half_log.unread == MAX_LEN // 2


def test_unread_full(full_log):
assert full_log.unread == MAX_LEN


def test_unread_read(read_log):
assert read_log.unread == 0


def test_get_unread_empty(empty_log):
assert empty_log.get_unread(1) == []
assert empty_log.get_unread(2) == []
assert empty_log.get_unread(MAX_LEN) == []


def test_get_unread_half(half_log):
assert half_log.get_unread(1) == [f'{1}']
assert half_log.get_unread(2) == [f'{2}', f'{3}']
assert half_log.get_unread(ALL) == [f'{i}' for i in range(4, MAX_LEN // 2 + 1)]


def test_get_unread_full(full_log):
assert full_log.get_unread(1) == [f'{1}']
assert full_log.get_unread(2) == [f'{2}', f'{3}']
assert full_log.get_unread(ALL) == [f'{i}' for i in range(4, MAX_LEN + 1)]


def test_get_unread_read(read_log):
assert read_log.get_unread(1) == []
assert read_log.get_unread(2) == []
assert read_log.get_unread(MAX_LEN) == []

0 comments on commit 5657c43

Please sign in to comment.