5 Commits

Author SHA1 Message Date
Yonokid
d8a219243c unlikely fix 2026-01-01 10:56:55 -05:00
Yonokid
e0b7f0a863 add sections to ai battle 2026-01-01 10:40:46 -05:00
Yonokid
7ef74a601b dancers can be removed 2025-12-31 14:45:08 -05:00
Yonokid
102d82001f Update README.md 2025-12-31 14:25:49 -05:00
Yonokid
f278868a83 better judgment improvements 2025-12-31 14:17:11 -05:00
10 changed files with 182 additions and 44 deletions

View File

@@ -265,7 +265,7 @@ def check_args():
selected_difficulty = args.difficulty
else:
selected_difficulty = max(tja.metadata.course_data.keys())
current_screen = Screens.GAME_PRACTICE if args.practice else Screens.GAME
current_screen = Screens.GAME_PRACTICE if args.practice else Screens.AI_GAME
global_data.session_data[PlayerNum.P1].selected_song = path
global_data.session_data[PlayerNum.P1].selected_difficulty = selected_difficulty
global_data.modifiers[PlayerNum.P1].auto = args.auto
@@ -392,10 +392,13 @@ def main():
logger.info("Cursor hidden")
last_fps = 1
last_color = pyray.BLACK
last_discord_check = 0
while not ray.WindowShouldClose():
if discord_connected:
current_time = get_current_ms()
if discord_connected and current_time > last_discord_check + 1000:
check_discord_heartbeat(current_screen)
last_discord_check = current_time
if ray.IsKeyPressed(global_data.config["keys"]["fullscreen_key"]):
ray.ToggleFullscreen()

View File

@@ -47,18 +47,30 @@ Just make sure to use `/` and not `\`!<br>
Q: I'm trying to play on Mac and it can't open!<br>
A: Delete your installation and follow these commands in the terminal:<br>
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
# Move to the place where you want to download Pytaiko, this example will be in the desktop
# Make sure the directory is not saved in iCloud! This will create an issue on macOS where the executable cannot compile.
cd ~/Desktop
# Installing dependencies
brew install python@3.12 uv git speexdsp libsndfile
# Downloading Pytaiko project
git clone https://github.com/Yonokid/PyTaiko
cd PyTaiko
brew install libsndfile
brew install speexdsp
# Compiling audio libraries
cd libs/audio
make
mv libaudio.dylib ../../
cd ../../
mv libaudio.dylib ...
cd ...
# Running game (for testing)
uv run PyTaiko.py
# Creating executable
uv add nuitka
uv run nuitka --mode=app --noinclude-setuptools-mode=nofollow --noinclude-IPython-mode=nofollow --assume-yes-for-downloads PyTaiko.py
```
## Installation
@@ -71,7 +83,7 @@ Download the latest release for your operating system from the [releases page](h
2. Run `PyTaiko.exe`
#### macOS
- Run with Python directly (see [Building from Source](#building-from-source))
- Run with Python directly or self compile (see [Building from Source](#building-from-source))
#### Linux
- Try running the compiled `PyTaiko.bin` binary

View File

@@ -130,10 +130,17 @@ class Background:
current_milestone = min(self.max_dancers - 1, int(gauge_1p.gauge_length / (clear_threshold / self.max_dancers)))
else:
current_milestone = self.max_dancers
if current_milestone > self.last_milestone and current_milestone <= self.max_dancers:
self.dancer.add_dancer()
self.last_milestone = current_milestone
logger.info(f"Dancer milestone reached: {current_milestone}/{self.max_dancers}")
elif current_milestone < self.last_milestone:
dancers_to_remove = self.last_milestone - current_milestone
for _ in range(dancers_to_remove):
self.dancer.remove_dancer()
self.last_milestone = current_milestone
logger.info(f"Dancer milestones lost: {current_milestone}/{self.max_dancers} (removed {dancers_to_remove})")
if self.bg_fever is not None and gauge_1p is not None:
if not self.is_clear and gauge_1p.is_clear:
self.bg_fever.start()

View File

@@ -162,6 +162,12 @@ class BaseDancerGroup():
dancer.start()
self.active_count += 1
def remove_dancer(self):
if self.active_count > 1:
self.active_count -= 1
position = self.spawn_positions[self.active_count]
self.active_dancers[position] = None
def update(self, current_time_ms: float, bpm: float):
for dancer in self.dancers:
dancer.update(current_time_ms, bpm)

View File

@@ -3,6 +3,7 @@ from typing import Any
from libs.audio import audio
from libs.texture import tex
from libs.utils import input_state
logger = logging.getLogger(__name__)
@@ -34,6 +35,7 @@ class Screen:
return next_screen
def update(self) -> Any:
input_state.update()
ret_val = self._do_screen_start()
if ret_val:
return ret_val

View File

@@ -1,10 +1,9 @@
import string
import ctypes
import hashlib
import sys
import logging
import string
import sys
import time
from libs.global_data import PlayerNum, global_data
from pathlib import Path
from typing import Optional
@@ -16,6 +15,7 @@ from raylib import (
SHADER_UNIFORM_VEC4,
)
from libs.global_data import PlayerNum, global_data
from libs.texture import TextureWrapper
logger = logging.getLogger(__name__)
@@ -57,11 +57,25 @@ def strip_comments(code: str) -> str:
index += 1
return result
class InputState:
def __init__(self):
self.pressed_keys_this_frame = set()
def update(self):
"""Call this once per frame to drain the key queue"""
self.pressed_keys_this_frame.clear()
key = rl.GetKeyPressed()
while key > 0:
self.pressed_keys_this_frame.add(key)
key = rl.GetKeyPressed()
input_state = InputState()
def is_input_key_pressed(keys: list[int], gamepad_buttons: list[int]):
if global_data.input_locked:
return False
for key in keys:
if rl.IsKeyPressed(key):
if key in input_state.pressed_keys_this_frame:
return True
if rl.IsGamepadAvailable(0):

View File

@@ -1,6 +1,8 @@
import copy
import logging
import random
import time
from enum import Enum
from pathlib import Path
from typing import Optional
@@ -13,7 +15,7 @@ from libs.chara_2d import Chara2D
from libs.global_data import Difficulty, Modifiers, PlayerNum, global_data
from libs.global_objects import Nameplate
from libs.texture import tex
from libs.tja import Note, TJAParser
from libs.tja import TJAParser
from libs.utils import get_current_ms, global_tex
from scenes.game import (
DrumType,
@@ -26,12 +28,23 @@ from scenes.game import (
logger = logging.getLogger(__name__)
class AIDifficulty(Enum):
LVL_1 = (0.90, 0.10)
LVL_2 = (0.92, 0.08)
LVL_3 = (0.94, 0.06)
LVL_4 = (0.96, 0.04)
LVL_5 = (0.98, 0.02)
def __iter__(self):
return iter(self.value)
class AIBattleGameScreen(GameScreen):
def on_screen_start(self):
super().on_screen_start()
session_data = global_data.session_data[global_data.player_num]
self.song_info = SongInfoAI(session_data.song_title, session_data.genre_index)
self.background = AIBackground(session_data.selected_difficulty)
self.section_board = SectionBoard()
def global_keys(self):
if ray.is_key_pressed(global_data.config["keys"]["restart_key"]):
@@ -89,20 +102,36 @@ class AIBattleGameScreen(GameScreen):
"""Initialize the TJA file"""
self.tja = TJAParser(song, start_delay=self.start_delay)
self.movie = None
global_data.session_data[global_data.player_num].song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
session_data = global_data.session_data[global_data.player_num]
session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None:
self.song_music = audio.load_music_stream(self.tja.metadata.wave, 'song')
tja_copy = copy.deepcopy(self.tja)
self.player_1 = PlayerNoChara(self.tja, global_data.player_num, global_data.session_data[global_data.player_num].selected_difficulty, False, global_data.modifiers[global_data.player_num])
self.player_1 = PlayerNoChara(self.tja, global_data.player_num, session_data.selected_difficulty, False, global_data.modifiers[global_data.player_num])
self.player_1.gauge = AIGauge(self.player_1.player_num, self.player_1.difficulty, self.tja.metadata.course_data[self.player_1.difficulty].level, self.player_1.total_notes, self.player_1.is_2p)
ai_modifiers = copy.deepcopy(global_data.modifiers[global_data.player_num])
ai_modifiers.auto = True
self.player_2 = AIPlayer(tja_copy, PlayerNum.AI, global_data.session_data[global_data.player_num].selected_difficulty, True, ai_modifiers)
self.player_2 = AIPlayer(tja_copy, PlayerNum.AI, session_data.selected_difficulty, True, ai_modifiers, AIDifficulty.LVL_2)
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
self.precise_start = time.time() - self.tja.metadata.offset
self.total_notes = len(self.player_1.don_notes) + len(self.player_1.kat_notes)
logger.info(f"TJA initialized for two-player song: {song}")
def update_scoreboards(self):
section_notes = (self.total_notes // 5) if self.section_board.num < 3 else (self.total_notes // 5) + (self.total_notes % 5) - 1
if self.player_1.good_count + self.player_1.ok_count + self.player_1.bad_count == section_notes:
self.player_2.good_percentage = self.player_1.good_count / section_notes
self.player_2.ok_percentage = self.player_1.ok_count / section_notes
logger.info(f"AI Good Percentage: {self.player_2.good_percentage}, AI OK Percentage: {self.player_2.ok_percentage}")
self.player_1.good_count, self.player_1.ok_count, self.player_1.bad_count = 0, 0, 0
self.player_2.good_count, self.player_2.ok_count, self.player_2.bad_count = 0, 0, 0
if self.background.contest_point >= 10:
self.section_board.wins[self.section_board.num] = True
else:
self.section_board.wins[self.section_board.num] = False
self.section_board.num += 1
def update(self):
super(GameScreen, self).update()
current_time = get_current_ms()
@@ -114,18 +143,12 @@ class AIBattleGameScreen(GameScreen):
self.start_ms = current_time - self.tja.metadata.offset*1000
self.update_background(current_time)
if self.song_music is not None:
audio.update_music_stream(self.song_music)
self.update_audio(self.current_ms)
self.player_1.update(self.current_ms, current_time, None)
self.player_2.update(self.current_ms, current_time, None)
section_notes = self.total_notes // 5
if self.player_1.good_count + self.player_1.ok_count + self.player_1.bad_count == section_notes:
self.player_2.good_percentage = self.player_1.good_count / section_notes
self.player_2.ok_percentage = self.player_1.ok_count / section_notes
logger.info(f"AI Good Percentage: {self.player_2.good_percentage}, AI OK Percentage: {self.player_2.ok_percentage}")
self.player_1.good_count, self.player_1.ok_count, self.player_1.bad_count = 0, 0, 0
self.player_2.good_count, self.player_2.ok_count, self.player_2.bad_count = 0, 0, 0
self.update_scoreboards()
self.section_board.update(current_time, self.player_1.good_count + self.player_1.ok_count + self.player_1.bad_count, self.total_notes)
self.song_info.update(current_time)
self.result_transition.update(current_time)
@@ -139,6 +162,8 @@ class AIBattleGameScreen(GameScreen):
if current_time >= self.end_ms + 1000:
if self.player_1.ending_anim is None:
self.spawn_ending_anims()
if self.player_1.modifiers.subdiff in [0, 1, 2, 3, 4, 5, 9, 13]:
self.write_score()
if current_time >= self.end_ms + 8533.34:
if not self.result_transition.is_started:
self.result_transition.start()
@@ -149,17 +174,63 @@ class AIBattleGameScreen(GameScreen):
return self.global_keys()
def update_background(self, current_time):
self.background.update(current_time, (self.player_1.good_count, self.player_1.ok_count), (self.player_2.good_count, self.player_2.ok_count))
if self.player_1.don_notes == self.player_2.don_notes and self.player_1.kat_notes == self.player_2.kat_notes:
self.background.update_values((self.player_1.good_count, self.player_1.ok_count), (self.player_2.good_count, self.player_2.ok_count))
self.background.update(current_time)
def draw(self):
if self.movie is not None:
self.movie.draw()
elif self.background is not None:
self.background.draw(self.player_1.chara, self.player_2.chara)
self.section_board.draw()
self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader)
self.player_2.draw(self.current_ms, self.start_ms, self.mask_shader)
self.draw_overlay()
class SectionBoard:
def __init__(self):
self.num = 0
self.wins: list[Optional[bool]] = [None] * 5
self.current_progress = 0
self.progress_bar_flash = Animation.create_fade(133, loop=True, reverse_delay=0)
self.section_highlight_flash = Animation.create_fade(350, loop=True, reverse_delay=0)
self.section_highlight_flash.start()
self.progress_bar_flash.start()
def update(self, current_time, player_notes, total_notes):
self.current_progress = player_notes / (total_notes // 5) if self.num < 3 else player_notes / ((total_notes // 5) + (total_notes % 5))
self.progress_bar_flash.update(current_time)
self.section_highlight_flash.update(current_time)
def draw(self):
if self.current_progress < 0.75:
color = ray.GREEN
fade = 1.0
else:
color = ray.YELLOW
fade = self.progress_bar_flash.attribute
ray.draw_rectangle(int(177 * tex.screen_scale), int(160 * tex.screen_scale), int(148 * tex.screen_scale), int(20 * tex.screen_scale), ray.GRAY)
ray.draw_rectangle(int(177 * tex.screen_scale), int(160 * tex.screen_scale), int(self.current_progress * (148 * tex.screen_scale)), int(20 * tex.screen_scale), ray.fade(color, fade))
tex.draw_texture('ai_battle', 'progress_bar')
if self.num < len(self.wins):
tex.draw_texture('ai_battle', 'section_text', index=0, frame=self.num)
if self.num < len(self.wins) - 1:
tex.draw_texture('ai_battle', 'section_text', index=1, frame=self.num+1)
tex.draw_texture('ai_battle', 'sections')
if self.num < len(self.wins):
tex.draw_texture('ai_battle', 'section_highlight_green', index=self.num)
tex.draw_texture('ai_battle', 'section_highlight_white', index=self.num, fade=self.section_highlight_flash.attribute)
for i in range(len(self.wins)):
if self.wins[i] is not None:
if self.wins[i]:
tex.draw_texture('ai_battle', 'section_win', index=i)
else:
tex.draw_texture('ai_battle', 'section_lose', index=i)
class PlayerNoChara(Player):
def __init__(self, tja: TJAParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers):
super().__init__(tja, player_num, difficulty, is_2p, modifiers)
@@ -211,7 +282,7 @@ class PlayerNoChara(Player):
margin = tex.textures["ai_battle"]["scoreboard_num"].width//2
total_width = len(str(counter)) * margin
for i, digit in enumerate(str(counter)):
tex.draw_texture('ai_battle', 'scoreboard_num', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation[j].attribute, y2=self.stretch_animation[j].attribute, index=j, controllable=True)
tex.draw_texture('ai_battle', 'scoreboard_num', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation[j].attribute, y2=self.stretch_animation[j].attribute, index=j)
# Group 8: Special animations and counters
if self.drumroll_counter is not None:
@@ -226,7 +297,7 @@ class PlayerNoChara(Player):
class AIPlayer(Player):
def __init__(self, tja: TJAParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers):
def __init__(self, tja: TJAParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers, ai_difficulty: AIDifficulty):
super().__init__(tja, player_num, difficulty, is_2p, modifiers)
self.stretch_animation = [tex.get_animation(5, is_copy=True) for _ in range(4)]
self.chara = Chara2D(player_num - 1, self.bpm)
@@ -235,8 +306,7 @@ class AIPlayer(Player):
self.gauge_hit_effect = []
plate_info = global_data.config[f'nameplate_{self.is_2p+1}p']
self.nameplate = Nameplate(plate_info['name'], plate_info['title'], PlayerNum.AI, plate_info['dan'], plate_info['gold'], plate_info['rainbow'], plate_info['title_bg'])
self.good_percentage = 0.90
self.ok_percentage = 0.07
self.good_percentage, self.ok_percentage = ai_difficulty
def update(self, ms_from_start: float, current_time: float, background: Optional[Background]):
good_count, ok_count, bad_count, total_drumroll = self.good_count, self.ok_count, self.bad_count, self.total_drumroll
@@ -339,7 +409,7 @@ class AIPlayer(Player):
margin = tex.textures["ai_battle"]["scoreboard_num"].width//2
total_width = len(str(counter)) * margin
for i, digit in enumerate(str(counter)):
tex.draw_texture('ai_battle', 'scoreboard_num', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation[j].attribute, y2=self.stretch_animation[j].attribute, index=j+4, controllable=True)
tex.draw_texture('ai_battle', 'scoreboard_num', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation[j].attribute, y2=self.stretch_animation[j].attribute, index=j+4)
# Group 8: Special animations and counters
if self.drumroll_counter is not None:
@@ -418,13 +488,18 @@ class AIBackground:
]
self.contest_point_fade = Animation.create_fade(166, initial_opacity=0.0, final_opacity=1.0, reverse_delay=166, delay=166, loop=True)
self.triangles_down = Animation.create_move(8500, total_distance=1152, loop=True)
self.contest_point_fade.start()
self.triangles_down.start()
def update(self, current_ms: float, player_judge: tuple[int, int], ai_judge: tuple[int, int]):
def update(self, current_ms: float):
self.contest_point_fade.update(current_ms)
self.triangles_down.update(current_ms)
def update_values(self, player_judge: tuple[int, int], ai_judge: tuple[int, int]):
player_total = (player_judge[0] * self.multipliers[self.difficulty][0]) + (player_judge[1] * self.multipliers[self.difficulty][1])
ai_total = (ai_judge[0] * self.multipliers[self.difficulty][0]) + (ai_judge[1] * self.multipliers[self.difficulty][1])
self.contest_point = player_total - ai_total + 10
self.contest_point = ((player_total - ai_total) // 2) + 10
self.contest_point = min(max(1, self.contest_point), self.total_tiles - 1)
def unload(self):
@@ -437,6 +512,9 @@ class AIBackground:
tex.draw_texture('ai_battle', 'red_tile_lower', frame=i, x=(i*tile_width))
for i in range(self.total_tiles - self.contest_point):
tex.draw_texture('ai_battle', 'blue_tile_lower', frame=i, x=(((self.total_tiles - 1) - i)*tile_width))
tex.draw_texture('ai_battle', 'lower_triangles_1', y=self.triangles_down.attribute, fade=0.5)
tex.draw_texture('ai_battle', 'lower_triangles_2', y=self.triangles_down.attribute, fade=0.5)
tex.draw_texture('ai_battle', 'highlight_tile_lower', x=self.contest_point * tile_width, fade=self.contest_point_fade.attribute)
def draw_upper(self, chara_1: Chara2D, chara_2: Chara2D):

View File

@@ -3,6 +3,7 @@ import logging
import pyray as ray
from libs.audio import audio
from libs.chara_2d import Chara2D
from libs.file_navigator import SongFile
from libs.global_data import Difficulty, PlayerNum, global_data
from libs.texture import tex
@@ -27,9 +28,11 @@ class AISongSelectScreen(SongSelectScreen):
super().on_screen_start()
self.player_1 = AISongSelectPlayer(global_data.player_num, self.text_fade_in)
global_data.modifiers[global_data.player_num].subdiff = 0
self.ai_chara = Chara2D(PlayerNum.AI-1)
def update_players(self, current_time) -> str:
self.player_1.update(current_time)
self.ai_chara.update(current_time)
if self.text_fade_out.is_finished:
self.player_1.selected_song = True
next_screen = "AI_GAME"
@@ -69,6 +72,7 @@ class AISongSelectScreen(SongSelectScreen):
tex.draw_texture('global', 'song_select', fade=self.text_fade_out.attribute)
self.draw_players()
self.ai_chara.draw(x=tex.skin_config["song_select_chara_2p"].x, y=tex.skin_config["song_select_chara_2p"].y, mirror=True)
if self.state == State.BROWSING and self.navigator.items != []:
curr_item = self.navigator.get_current_item()
@@ -183,12 +187,8 @@ class AISongSelectPlayer(SongSelectPlayer):
offset = 0
if self.subdiff_selector is not None:
offset = -self.subdiff_selector.move.attribute*1.05
if self.player_num == PlayerNum.P1:
self.nameplate.draw(tex.skin_config["song_select_nameplate_1p"].x, tex.skin_config["song_select_nameplate_1p"].y)
self.chara.draw(x=tex.skin_config["song_select_chara_1p"].x, y=tex.skin_config["song_select_chara_1p"].y + (offset*0.6))
else:
self.nameplate.draw(tex.skin_config["song_select_nameplate_2p"].x, tex.skin_config["song_select_nameplate_2p"].y)
self.chara.draw(mirror=True, x=tex.skin_config["song_select_chara_2p"].x, y=tex.skin_config["song_select_chara_2p"].y + (offset*0.6))
if self.subdiff_selector is not None:
self.subdiff_selector.draw()

View File

@@ -2,6 +2,7 @@ import bisect
import logging
import math
import sqlite3
import time
from collections import deque
from enum import IntEnum
from itertools import chain
@@ -77,6 +78,7 @@ class GameScreen(Screen):
self.paused = False
self.pause_time = 0
self.audio_time = 0
self.last_resync = 0
self.movie = None
self.song_music = None
if global_data.config["general"]["nijiiro_notes"]:
@@ -153,6 +155,7 @@ class GameScreen(Screen):
self.player_1 = Player(self.tja, global_data.player_num, global_data.session_data[global_data.player_num].selected_difficulty, False, global_data.modifiers[global_data.player_num])
self.start_ms = get_current_ms() - self.tja.metadata.offset*1000
self.precise_start = time.time() - self.tja.metadata.offset
def write_score(self):
"""Write the score to the database"""
@@ -249,6 +252,20 @@ class GameScreen(Screen):
if self.background is not None:
self.background.update(current_time, self.bpm, self.player_1.gauge, None)
def update_audio(self, ms_from_start: float):
if not self.song_started:
return
if self.song_music is not None:
audio.update_music_stream(self.song_music)
'''
raw_audio_time = audio.get_music_time_played(self.song_music)
current_time = time.time() - self.precise_start
if abs(raw_audio_time - current_time) >= 0.006 and current_time > self.last_resync + 1000:
audio.seek_music_stream(self.song_music, current_time)
logger.info(f"Resyncing due to difference: {raw_audio_time} - {current_time} = {raw_audio_time - current_time}")
self.last_resync = current_time
'''
def update(self):
super().update()
current_time = get_current_ms()
@@ -261,8 +278,7 @@ class GameScreen(Screen):
self.start_ms = current_time - self.tja.metadata.offset*1000
self.update_background(current_time)
if self.song_music is not None:
audio.update_music_stream(self.song_music)
self.update_audio(self.current_ms)
self.player_1.update(self.current_ms, current_time, self.background)
self.song_info.update(current_time)