Let's see if this is working now. When I have some issues, I change to my second computer, sometimes it works with a new computer.

This commit is contained in:
Seoyeon Lee 2024-12-17 12:52:56 -05:00
parent 77d4e803ee
commit cf5353e091
8 changed files with 440 additions and 97 deletions

131
board.py Normal file
View File

@ -0,0 +1,131 @@
from common import HOR_SEP_LEN, VER_SEP_LEN, PAD
class Tile:
def __init__(self, path, step):
self.path = path
self.step = step
self.name = f'{path}_{step}'
self.character = 'o'
self.make_position()
self.make_big_tile()
self.z = 1
def make_big_tile(self):
peri_big = self.path == 'peri' and self.step in [0, 5, 10, 15]
diag_big = self.path.startswith('diag') and self.step == 3
if peri_big or diag_big:
self.character = 'O'
self.color = 'purple'
def make_position(self):
min_x = PAD
max_x = PAD + (HOR_SEP_LEN + 1) * 5
min_y = PAD
max_y = PAD + (VER_SEP_LEN + 1) * 5
x_step = HOR_SEP_LEN + 1
y_step = VER_SEP_LEN + 1
if self.path == 'peri':
if self.step <= 5:
x = min_x + x_step * self.step
y = max_y
elif self.step <= 10:
x = max_x
y = min_y + y_step * (10 - self.step)
elif self.step <= 15:
x = min_x + x_step * (15 - self.step)
y = PAD
elif self.step <= 19:
x = min_x
y = min_y + y_step * (self.step - 15)
if self.path == 'diag1':
if self.step == 1:
x = max_x - (HOR_SEP_LEN + 1)
y = min_y + (VER_SEP_LEN + 1)
else:
x = max_x - (HOR_SEP_LEN + 1) - HOR_SEP_LEN*(self.step - 1)
y = min_y + (VER_SEP_LEN + 1) + VER_SEP_LEN*(self.step - 1)
if self.path == 'diag2':
if self.step == 1:
x = max_x - (HOR_SEP_LEN + 1)
y = max_y - (VER_SEP_LEN + 1)
else:
x = max_x - (HOR_SEP_LEN + 1) - HOR_SEP_LEN*(self.step - 1)
y = max_y - (VER_SEP_LEN + 1) - VER_SEP_LEN*(self.step - 1)
self.position = (x, y)
class Separator:
def __init__(self, kind, position):
chars = {
'hor': '-',
'ver': '|',
'diagr': '/',
'diagl': '\\',
}
self.character = chars[kind]
self.position = position
self.z = 0
def make_board():
agents = []
#Perimeter loop
peri_agents = [Tile('peri', step) for step in range(20)]
agents.extend(peri_agents)
# First diagonal
diag1_agents = [Tile('diag1', step) for step in range(1, 6)]
agents.extend(diag1_agents)
# Second diagonal
diag2_agents = [Tile('diag2', step) for step in range(1, 6)]
agents.extend(diag2_agents)
# Separators
agents.extend(make_separators(peri_agents))
agents.extend(make_separators(diag1_agents))
agents.extend(make_separators(diag2_agents))
# Manually add the remaining separators
agents.extend([Separator('diagr', (PAD+i, PAD - i + (VER_SEP_LEN + 1) * 5)) for i in range(1, HOR_SEP_LEN + 1)])
agents.extend([Separator('diagr', (PAD+(HOR_SEP_LEN+1)*5 - i, PAD + i)) for i in range(1, VER_SEP_LEN + 1)])
agents.extend([Separator('diagl', (PAD+i, PAD + i)) for i in range(1, HOR_SEP_LEN + 1)])
agents.extend([Separator('diagl', (PAD+(HOR_SEP_LEN+1)*5 - i, PAD + (VER_SEP_LEN+1) * 5 - i)) for i in range(1, VER_SEP_LEN + 1)])
return agents
def make_separators(tiles):
agents = []
for i in range(len(tiles)):
this_tile = tiles[i]
if i == len(tiles)-1:
next_tile = tiles[0]
else:
next_tile = tiles[i+1]
x0 = this_tile.position[0]
y0 = this_tile.position[1]
dx = next_tile.position[0] - x0
dy = next_tile.position[1] - y0
if dx > 0 and dy == 0:
agents.extend([Separator('hor', (x0+1+step, y0)) for step in range(HOR_SEP_LEN)])
if dx < 0 and dy == 0:
agents.extend([Separator('hor', (x0-1-step, y0)) for step in range(HOR_SEP_LEN)])
if dx == 0 and dy > 0:
agents.extend([Separator('ver', (x0, y0+1+step)) for step in range(VER_SEP_LEN)])
if dx == 0 and dy < 0:
agents.extend([Separator('ver', (x0, y0-1-step)) for step in range(VER_SEP_LEN)])
if dx < 0 and dy > 0:
agents.extend([Separator('diagr', (x0-1-step, y0+1+step)) for step in range(VER_SEP_LEN)])
if dx < 0 and dy < 0:
agents.extend([Separator('diagl', (x0-1-step, y0-1-step)) for step in range(VER_SEP_LEN)])
return agents

15
common.py Normal file
View File

@ -0,0 +1,15 @@
HOR_SEP_LEN = 3
VER_SEP_LEN = 3
DIAG_SEP_INNER_LEN = 2
DIAG_SEP_OUTER_LEN = 3
PAD = 4
TOKENS_PER_PLAYER = 4
PLAYER_COLORS = {
0: 'blue',
1: 'red',
}
PLAYER_NAMES = {
0: '1 (Blue)',
1: '2 (Red)',
}

182
gm.py Normal file
View File

@ -0,0 +1,182 @@
from retro.errors import AgentNotFoundByName
import random
from common import TOKENS_PER_PLAYER, PLAYER_NAMES
class GameMaster:
def __init__(self, width):
self.name = 'gm'
self.display = False
self.position=(0,0)
self.state = 'welcome'
self.player = 0
self.width = width
self.current_token = None
self.die_roll = 0
self.move_count = 0
self.completed_tokens = {0: 0, 1: 0}
def message(self, game, msg1='', msg2=''):
game.state['msg1'] = msg1.ljust(self.width)
game.state['msg2'] = msg2.ljust(self.width)
def play_turn(self, game):
game.log(f'State is {self.state}')
if self.state == 'welcome':
self.message(game, 'Welcome to Yutnori!', 'Press ENTER to begin')
game.state['Current Player'] = PLAYER_NAMES[self.player]
return
if self.state == 'ask_throw_sticks':
color = PLAYER_NAMES[self.player]
self.message(game, f'Player {color}, throw your sticks!', 'Press ENTER to throw')
return
if self.state == 'determine_path':
tok = self.current_token
if tok.path == 'none':
tok.path = 'peri'
tok.step = 0
self.state = 'moving_token'
return
if tok.path == 'peri':
if tok.step == 5:
self.state = 'prompt_lr_corner_path_choice'
return
if tok.step == 10:
self.state = 'prompt_tr_corner_path_choice'
return
if tok.step == 15:
self.state = 'prompt_tl_corner_path_choice'
return
self.state = 'moving_token'
if 'diag' in tok.path:
if tok.step == 3:
self.state = 'prompt_center_choice'
return
self.state = 'moving_token'
return
if self.state == 'moving_token':
if self.move_count == 0:
self.current_token.kick_out_enemy_tokens(game)
self.current_token.adjust_position(game)
self.state = 'check_next_player'
if not game.turn_number % 3:
game.log(f'Moves remaining: {self.move_count}')
self.current_token.step_forward(game)
if self.current_token.step == 0 and self.current_token.path == 'peri':
game.remove_agent(self.current_token)
self.completed_tokens[self.player] = self.completed_tokens[self.player] + 1
self.state = 'check_next_player'
return
self.move_count = self.move_count - 1
return
if self.state == 'prompt_lr_corner_path_choice':
self.message(game, f'Where would you like to go?', '(P)erimeter or (D)iagonal?')
if self.state == 'prompt_tr_corner_path_choice':
self.message(game, f'Where would you like to go?', '(P)erimeter or (D)iagonal?')
if self.state == 'prompt_tl_corner_path_choice':
self.message(game, f'Where would you like to go?', '(P)erimeter or (D)iagonal?')
if self.state == 'prompt_center_choice':
msg1 = 'Where would you like to go?'
if self.current_token.path == 'diag2':
msg2 = '(H)ome, Top (L)eft, Top (R)ight'
if self.current_token.path == 'diag1':
msg2 = '(H)ome, Top (L)eft, Bottom (R)ight'
if self.current_token.path == 'diag2rev':
msg2 = '(H)ome, (T)op Right, (B)ottom Right'
self.message(game, msg1, msg2)
if self.state == 'check_next_player':
# Check if this player has won
if self.completed_tokens[self.player] == TOKENS_PER_PLAYER:
self.message(game, 'CONGRATULATIONS!!', f'Player {self.player} wins Yutnori!!')
game.end()
# Check if the previous roll was a Yut
if self.die_roll != 5:
self.player = 0 if self.player == 1 else 1
game.state['Current Player'] = PLAYER_NAMES[self.player]
self.state = 'ask_throw_sticks'
def handle_keystroke(self, keystroke, game):
if self.state == 'welcome':
if keystroke.name == 'KEY_ENTER':
self.state = 'ask_throw_sticks'
game.log(f'State is {self.state}')
return
if self.state == 'ask_throw_sticks':
if keystroke.name == 'KEY_ENTER':
values = {
1: 'Pig',
2: 'Dog',
3: 'Lamb',
4: 'Cow',
5: 'Horse',
}
die = random.randint(1, 5)
self.message(game, f'You rolled {die} ({values[die]})', 'Choose a token to move (1-4)')
self.state = 'choose_token'
self.die_roll = die
self.move_count = die
game.log(f'State is {self.state}')
return
if self.state == 'choose_token':
if not keystroke.isdigit():
return
try:
self.current_token = game.get_agent_by_name(f'{self.player}_{keystroke}')
except AgentNotFoundByName:
return
self.state = 'determine_path'
if self.state == 'prompt_lr_corner_path_choice':
if keystroke.lower() == 'p':
self.current_token.path = 'peri'
self.state = 'moving_token'
elif keystroke.lower() == 'd':
self.current_token.path = 'diag2'
self.current_token.step = 0
self.state = 'moving_token'
if self.state == 'prompt_tr_corner_path_choice':
if keystroke.lower() == 'p':
self.current_token.path = 'peri'
self.state = 'moving_token'
elif keystroke.lower() == 'd':
self.current_token.path = 'diag1'
self.current_token.step = 0
self.state = 'moving_token'
if self.state == 'prompt_tl_corner_path_choice':
if keystroke.lower() == 'p':
self.current_token.path = 'peri'
self.state = 'moving_token'
elif keystroke.lower() == 'd':
self.current_token.path = 'diag2rev'
self.current_token.step = 6
self.state = 'moving_token'
if self.state == 'prompt_center_choice':
if keystroke.lower() == 'h':
self.current_token.path = 'diag1'
self.state = 'moving_token'
if keystroke.lower() == 'l':
self.current_token.path = 'diag2'
self.state = 'moving_token'
if keystroke.lower() in ['r', 't']:
self.current_token.path = 'diag2rev'
self.state = 'moving_token'
if keystroke.lower() == 'b':
self.current_token.path = 'diag1rev'
self.state = 'moving_token'

88
poetry.lock generated
View File

@ -1,89 +1,7 @@
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
[[package]]
name = "ansicon"
version = "1.89.0"
description = "Python wrapper for loading Jason Hood's ANSICON"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
]
[[package]]
name = "blessed"
version = "1.20.0"
description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities."
category = "main"
optional = false
python-versions = ">=2.7"
files = [
{file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"},
{file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"},
]
[package.dependencies]
jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""}
six = ">=1.9.0"
wcwidth = ">=0.1.4"
[[package]]
name = "jinxed"
version = "1.3.0"
description = "Jinxed Terminal Library"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5"},
{file = "jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf"},
]
[package.dependencies]
ansicon = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "retro-games"
version = "1.1.0"
description = "A simple framework for Terminal-based games"
category = "main"
optional = false
python-versions = "<4.0,>=3.10"
files = [
{file = "retro_games-1.1.0-py3-none-any.whl", hash = "sha256:c621117e4dd528b1e4870d897d00c4365566ab3ba965177e3996ed3c889dd9f8"},
{file = "retro_games-1.1.0.tar.gz", hash = "sha256:2167b574f42fe1e739b7c9ec75e98a9b76df42e2166376b85559291b3dc58f82"},
]
[package.dependencies]
blessed = ">=1.20.0,<2.0.0"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
package = []
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "2bcb10c06051310c2d2ce4f7b65ed30705765ffe70121fe098b07e32ef9307bc"
content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9"

View File

@ -1,7 +1,7 @@
# Game proposal
## Team
This will be a solo work a player completing with a computer.
Two players play. Play 1 with one color token (blue) player 2 with another color (red).
## Game Overview
I plan to do Yunori for my game project. This is a traditional Korean game I used to play with all my relatives when I was a kid. Game itself is very simple, but it is surprisingly exciting! With computer version, the trill of throwing four wooden stikcs into the air at once will be missing, and no sounds of clatter of the sticks when it colliding to the mat on the ground. However, this kind of computer version would be a good way to teach or learn how to move 4 tokens stratigically to win the game. Each payer or each team starts with 4 stones (so we need either two different colors or two different types), and the four tokens travels the entire board and come back to the starting point, then the player win.
@ -14,6 +14,8 @@ Two different color of tokens cannot share the same spot. When a player's token
## Milestone
I don't know how to do TFFF, TTFF, TTTF, TTTT, FFFF with graphical representation of Yut wood stick on the screen. But, I think I can simply this with the idea of using a die. 1=Do, 2=Gae, 3=Girl, 4=Yut, and Mo=5. So, it is like a throwing a die with five faces.
Drowing a board as it should look. Inteonally thinking of a square board, but feeling it's even harder.... To be able to play the game, the shape of the board does not really need to be perfect square, so I will do a rectangule shape of board for now. All the spots need to be included.
Intially, I thought I would do only one token to simplfy the version of the game, I am still working on how to make four token can be used on the board at the same time.
## Challenges
I am not sure how to have two different options (going forward in square board vs. making a diagonal path) and how to do the decision part with tokens (using which token on the board if there is multiple tokens on the board already vs. using a new token).

View File

@ -7,7 +7,6 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
retro-games = "^1.1.0"
[build-system]

93
token.py Normal file
View File

@ -0,0 +1,93 @@
from common import PLAYER_COLORS
class Token:
def __init__(self, player, number, pad):
self.home_position = (pad+number-1, pad+20+player+2)
self.position = self.home_position
self.player = player
self.color = PLAYER_COLORS[player]
self.character = str(number)
self.name = f'{player}_{number}'
self.z = 10
self.path = 'none'
self.step = -1
def go_home(self):
self.position = self.home_position
self.path = 'none'
self.step = -1
def step_forward(self, game):
if self.path == 'peri':
path_to_search = 'peri'
self.step = self.step + 1
if self.step == 20:
self.step = 0
elif self.path == 'diag1':
path_to_search = 'diag1'
if self.step == 5:
self.step = 0
self.path = 'peri'
path_to_search = 'peri'
else:
self.step = self.step + 1
elif self.path == 'diag2':
path_to_search = 'diag2'
if self.step == 5:
self.step = 15
self.path = 'peri'
path_to_search = 'peri'
else:
self.step = self.step + 1
elif self.path == 'diag2rev':
path_to_search = 'diag2'
if self.step == 1:
self.step = 5
self.path = 'peri'
path_to_search = 'peri'
else:
self.step = self.step - 1
elif self.path == 'diag1rev':
path_to_search = 'diag1'
if self.step == 1:
self.step = 10
self.path = 'peri'
path_to_search = 'peri'
else:
self.step = self.step - 1
tile_name = f'{path_to_search}_{self.step}'
game.log(f'Going to tile {tile_name}')
dest_tile = game.get_agent_by_name(tile_name)
self.position = dest_tile.position
self.dest_position = dest_tile.position
def kick_out_enemy_tokens(self, game):
tokens = [
agent for agent in game.agents
if isinstance(agent, Token)
and self.path == agent.path
and self.step == agent.step
]
for token in tokens:
if token.player != self.player:
token.go_home()
def adjust_position(self, game):
tokens = [
agent for agent in game.agents
if isinstance(agent, Token)
and self.path == agent.path
and self.step == agent.step
]
if len(tokens) == 1:
return
max_x = max([token.position[0] for token in tokens])
min_y = min([token.position[1] for token in tokens])
if self.path == 'peri':
if self.step >= 0 and self.step < 5 or self.step > 10 and self.step < 15:
new_position = (self.position[0], min_y - 1)
self.position = new_position

View File

@ -1,13 +1,16 @@
from retro.game import Game
from gm import GameMaster
from token import Token
from board import make_board
from common import PAD
board_size = (30, 30)
class GameMaster:
def __init__(self):
self.name = 'gm'
self.display = False
game = Game(
[], {" ": "Welcome to Yutnori"}, board_size=board_size
)
game.play()
board_size = (PAD*2+20+20, PAD*2+25)
gm = GameMaster(board_size[0])
agents = [gm]
agents.extend(make_board())
for player in range(2):
for num_token in range(1, 5):
agents.append(Token(player, num_token, PAD))
game = Game(agents, state={'Current Player': '', 'msg1': '', 'msg2': ''}, board_size=board_size, debug=False)
game.play()