8 Commits
0.1 ... dev

2 changed files with 332 additions and 173 deletions

299
app.py
View File

@@ -8,11 +8,11 @@ app.config['SECRET_KEY'] = 'your_secret_key_here'
socketio = SocketIO(app, cors_allowed_origins="*")
# 内存存储
rooms = {} # room_id -> {name, password, public, board, turn, players, players_sid, watchers, user_color}
users = {} # sid -> username
username_to_sid = {} # username -> sid
rooms = {}
users = {}
username_to_sid = {}
# 初始化棋盘
# ---------- 棋盘初始化 ----------
def init_board():
return [
['r', 'h', 'e', 'a', 'k', 'a', 'e', 'h', 'r'],
@@ -30,21 +30,207 @@ def init_board():
def board_to_string(board):
return '\n'.join(''.join(row) for row in board)
# 中国象棋规则验证(保持不变)
# ---------- 规则验证 ----------
def is_valid_move(board, fr, fc, tr, tc, turn):
# ... 保持原有的规则验证代码不变 ...
# (这里省略,请保留之前的完整规则验证代码)
pass
"""检查移动是否合法"""
# 边界检查
if not (0 <= fr < 10 and 0 <= fc < 9 and 0 <= tr < 10 and 0 <= tc < 9):
return False
piece = board[fr][fc]
target = board[tr][tc]
# 起点不能为空
if piece == '.':
return False
# 检查是否属于当前方
if turn == 'red' and not piece.isupper():
return False
if turn == 'black' and not piece.islower():
return False
# 目标格不能有己方棋子
if target != '.':
if (turn == 'red' and target.isupper()) or (turn == 'black' and target.islower()):
return False
p = piece.lower()
dr = tr - fr
dc = tc - fc
# ---------- 车 ----------
if p == 'r':
if fr != tr and fc != tc:
return False
step_r = 1 if dr > 0 else -1 if dr < 0 else 0
step_c = 1 if dc > 0 else -1 if dc < 0 else 0
r, c = fr + step_r, fc + step_c
while (r, c) != (tr, tc):
if board[r][c] != '.':
return False
r += step_r
c += step_c
return True
# ---------- 马 ----------
elif p == 'h':
if (abs(dr), abs(dc)) not in [(2, 1), (1, 2)]:
return False
# 绊马腿
if abs(dr) == 2:
block_r, block_c = fr + (1 if dr > 0 else -1), fc
else:
block_r, block_c = fr, fc + (1 if dc > 0 else -1)
if board[block_r][block_c] != '.':
return False
return True
# ---------- 相/象 ----------
elif p == 'e':
if abs(dr) != 2 or abs(dc) != 2:
return False
# 不能过河
if turn == 'red' and tr < 5: # 红相不能过河到对方半场
return False
if turn == 'black' and tr > 4: # 黑象不能过河
return False
# 象眼
block_r, block_c = fr + (1 if dr > 0 else -1), fc + (1 if dc > 0 else -1)
if board[block_r][block_c] != '.':
return False
return True
# ---------- 士 ----------
elif p == 'a':
if abs(dr) != 1 or abs(dc) != 1:
return False
# 九宫
if turn == 'red':
if tr < 7 or tc < 3 or tc > 5:
return False
else:
if tr > 2 or tc < 3 or tc > 5:
return False
return True
# ---------- 将/帅 ----------
elif p == 'k':
if abs(dr) + abs(dc) != 1:
return False
# 九宫
if turn == 'red':
if tr < 7 or tc < 3 or tc > 5:
return False
else:
if tr > 2 or tc < 3 or tc > 5:
return False
return True
# ---------- 炮 ----------
elif p == 'c':
if fr != tr and fc != tc:
return False
step_r = 1 if dr > 0 else -1 if dr < 0 else 0
step_c = 1 if dc > 0 else -1 if dc < 0 else 0
r, c = fr + step_r, fc + step_c
cnt = 0
while (r, c) != (tr, tc):
if board[r][c] != '.':
cnt += 1
r += step_r
c += step_c
# 吃子:需要恰好一个炮架
if target != '.':
return cnt == 1
# 不吃子:路径必须为空
else:
return cnt == 0
# ---------- 兵/卒 ----------
elif p == 'p':
if turn == 'red':
if dr > 0: # 不能后退
return False
# 未过河(行 > 4只能前进
if fr > 4:
return dc == 0 and dr == -1
else: # 过河
return (dr == -1 and dc == 0) or (dr == 0 and abs(dc) == 1)
else: # 黑方
if dr < 0:
return False
if fr < 5: # 未过河
return dc == 0 and dr == 1
else: # 过河
return (dr == 1 and dc == 0) or (dr == 0 and abs(dc) == 1)
return False
def is_check(board, turn):
# ... 保持原有的将判断代码不变 ...
pass
"""检查 turn 方是否被将"""
# 找出将帅位置
red_king = black_king = None
for i in range(10):
for j in range(9):
if board[i][j] == 'K':
red_king = (i, j)
elif board[i][j] == 'k':
black_king = (i, j)
if turn == 'red':
if not red_king:
return True # 红方无帅,输
# 检查黑方所有棋子能否吃到红帅
for i in range(10):
for j in range(9):
piece = board[i][j]
if piece.islower() and piece != '.':
if is_valid_move(board, i, j, red_king[0], red_king[1], 'black'):
return True
# 将帅对面
if black_king and red_king[1] == black_king[1]:
c = red_king[1]
r1, r2 = sorted([red_king[0], black_king[0]])
for r in range(r1 + 1, r2):
if board[r][c] != '.':
break
else:
return True # 对面
return False
else:
if not black_king:
return True
for i in range(10):
for j in range(9):
piece = board[i][j]
if piece.isupper() and piece != '.':
if is_valid_move(board, i, j, black_king[0], black_king[1], 'red'):
return True
if red_king and red_king[1] == black_king[1]:
c = red_king[1]
r1, r2 = sorted([red_king[0], black_king[0]])
for r in range(r1 + 1, r2):
if board[r][c] != '.':
break
else:
return True
return False
def make_move(board, fr, fc, tr, tc, turn):
# ... 保持原有的移动代码不变 ...
pass
"""执行移动,如果合法返回新棋盘,否则返回 None"""
if not is_valid_move(board, fr, fc, tr, tc, turn):
return None
new_board = copy.deepcopy(board)
piece = new_board[fr][fc]
new_board[fr][fc] = '.'
new_board[tr][tc] = piece
# 检查移动后是否导致己方被将
if is_check(new_board, turn):
return None
return new_board
# 路由
# ---------- 路由 ----------
@app.route('/')
def lobby():
return render_template('lobby.html')
@@ -55,7 +241,7 @@ def room_page(room_id):
return "房间不存在", 404
return render_template('room.html', room_id=room_id)
# SocketIO 事件
# ---------- SocketIO 事件 ----------
@socketio.on('connect')
def handle_connect():
print('Client connected', request.sid)
@@ -131,7 +317,7 @@ def handle_create_room(data):
public = data['public']
password = data.get('password', '') if not public else ''
room_id = str(uuid.uuid4())[:8]
rooms[room_id] = {
'name': room_name,
'password': password,
@@ -143,13 +329,12 @@ def handle_create_room(data):
'watchers': {},
'user_color': {}
}
# 创建者自动加入房间(红方)
join_room(room_id)
rooms[room_id]['players']['red'] = username
rooms[room_id]['players_sid']['red'] = request.sid
rooms[room_id]['user_color'][username] = 'red'
emit('room_created', {'room_id': room_id}, room=request.sid)
broadcast_rooms()
@@ -159,33 +344,30 @@ def handle_join_room(data):
if not username:
emit('error', {'msg': '未登录'})
return
room_id = data['room_id']
password = data.get('password', '')
if room_id not in rooms:
emit('error', {'msg': '房间不存在'})
return
room = rooms[room_id]
# 验证密码
if room['password'] and room['password'] != password:
emit('error', {'msg': '密码错误'})
return
# 检查用户是否已经在房间中(重连)
# 重连处理
if username in room['user_color']:
color = room['user_color'][username]
old_sid = room['players_sid'][color]
if old_sid and old_sid != request.sid:
# 通知旧连接被踢出
emit('force_disconnect', {'msg': '您在另一个窗口加入了房间'}, room=old_sid)
disconnect(old_sid)
room['players_sid'][color] = request.sid
join_room(room_id)
emit('room_joined', {
'board': board_to_string(room['board']),
'turn': room['turn'],
@@ -194,24 +376,20 @@ def handle_join_room(data):
'watchers': len(room['watchers'])
}, room=request.sid)
return
# 新用户加入
# 检查红方位置是否可用
# 分配颜色
if room['players']['red'] is None:
color = 'red'
# 检查黑方位置是否可用
elif room['players']['black'] is None:
color = 'black'
else:
color = None # 旁观者
color = None
if color:
# 玩家身份
room['players'][color] = username
room['players_sid'][color] = request.sid
room['user_color'][username] = color
join_room(room_id)
emit('room_joined', {
'board': board_to_string(room['board']),
'turn': room['turn'],
@@ -219,17 +397,11 @@ def handle_join_room(data):
'players': room['players'],
'watchers': len(room['watchers'])
}, room=request.sid)
# 通知房间内其他玩家
emit('player_joined', {'color': color, 'username': username}, room=room_id, include_self=False)
print(f"玩家 {username} 加入房间 {room_id} 作为 {color}")
else:
# 旁观者
room['watchers'][request.sid] = username
join_room(room_id)
emit('room_joined', {
'board': board_to_string(room['board']),
'turn': room['turn'],
@@ -237,11 +409,9 @@ def handle_join_room(data):
'players': room['players'],
'watchers': len(room['watchers'])
}, room=request.sid)
emit('watcher_joined', {'username': username}, room=room_id, include_self=False)
print(f"玩家 {username} 加入房间 {room_id} 作为旁观者")
broadcast_rooms()
@socketio.on('move')
@@ -251,40 +421,49 @@ def handle_move(data):
if not username:
emit('error', {'msg': '未登录'})
return
room_id = data['room_id']
if room_id not in rooms:
emit('error', {'msg': '房间不存在'})
return
room = rooms[room_id]
# 检查是否轮到该玩家
# 回合检查
if room['turn'] == 'red' and room['players_sid']['red'] != sid:
emit('error', {'msg': '不是你的回合'})
emit('move_result', {'success': False, 'message': '不是你的回合'})
return
if room['turn'] == 'black' and room['players_sid']['black'] != sid:
emit('error', {'msg': '不是你的回合'})
emit('move_result', {'success': False, 'message': '不是你的回合'})
return
fr, fc = data['from']['row'], data['from']['col']
tr, tc = data['to']['row'], data['to']['col']
print(f"玩家 {username} 移动: ({fr},{fc}) ({tr},{tc})")
print(f"玩家 {username} 尝试移动: ({fr},{fc}) -> ({tr},{tc})")
print(f"当前棋盘:")
for row in room['board']:
print(' '.join(row))
new_board = make_move(room['board'], fr, fc, tr, tc, room['turn'])
if new_board is None:
emit('error', {'msg': '非法移动'})
emit('move_result', {'success': False, 'message': '非法移动'})
return
room['board'] = new_board
room['turn'] = 'black' if room['turn'] == 'red' else 'red'
# 广播新棋盘
print(f"移动成功,新棋盘:")
for row in room['board']:
print(' '.join(row))
print(f"下一回合: {room['turn']}")
emit('board_updated', {
'board': board_to_string(room['board']),
'turn': room['turn']
}, room=room_id)
emit('move_result', {'success': True}, room=sid)
if __name__ == '__main__':
socketio.run(app, debug=True, host='0.0.0.0', port=5000)
socketio.run(app, debug=True,allow_unsafe_werkzeug=True, host='0.0.0.0', port=5000)

View File

@@ -8,7 +8,7 @@ if (!username) {
}
const socket = io();
let myColor = null; // 'red', 'black', null(旁观)
let myColor = null; // 'red', 'black', null
let board = [];
let turn = 'red';
let selectedRow = -1, selectedCol = -1;
@@ -16,58 +16,52 @@ let selectedRow = -1, selectedCol = -1;
const canvas = document.getElementById('chessboard');
const ctx = canvas.getContext('2d');
const cellSize = 60;
const offset = 30; // 棋盘边距
const offset = 30;
// 修复坐标计算函数
// 精确坐标转换
function getBoardPosition(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
// 计算点击位置相对于canvas画布的比例
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// 计算在canvas坐标系中的实际坐标
const canvasX = (clientX - rect.left) * scaleX;
const canvasY = (clientY - rect.top) * scaleY;
// 减去偏移量后计算格子索引
const col = Math.floor((canvasX - offset) / cellSize);
const row = Math.floor((canvasY - offset) / cellSize);
const x = canvasX - offset;
const y = canvasY - offset;
const col = Math.floor(x / cellSize);
const row = Math.floor(y / cellSize);
if (x < 0 || y < 0 || col >= 9 || row >= 10) {
return { row: -1, col: -1 };
}
return { row, col };
}
// 绘制棋盘
function drawBoard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 画棋盘线
// 画线
ctx.lineWidth = 2;
ctx.strokeStyle = '#000';
// 画横线
ctx.strokeStyle = '#8B4513';
for (let i = 0; i < 10; i++) {
ctx.beginPath();
ctx.moveTo(offset, offset + i * cellSize);
ctx.lineTo(offset + 8 * cellSize, offset + i * cellSize);
ctx.stroke();
}
// 画竖线
for (let i = 0; i < 9; i++) {
ctx.beginPath();
ctx.moveTo(offset + i * cellSize, offset);
ctx.lineTo(offset + i * cellSize, offset + 9 * cellSize);
ctx.stroke();
}
// 楚河汉界
// 楚河汉界
ctx.font = 'bold 24px "KaiTi"';
ctx.fillStyle = '#333';
ctx.fillText('楚 河', offset + 2 * cellSize, offset + 4.5 * cellSize);
ctx.fillText('汉 界', offset + 2 * cellSize, offset + 5.5 * cellSize);
// 九宫对角线
ctx.fillStyle = '#8B4513';
ctx.fillText('楚 河', offset + 2 * cellSize, offset + 4.5 * cellSize - 5);
ctx.fillText('汉 界', offset + 2 * cellSize, offset + 5.5 * cellSize - 5);
// 九宫对角线
ctx.beginPath();
ctx.moveTo(offset + 3 * cellSize, offset);
ctx.lineTo(offset + 5 * cellSize, offset + 2 * cellSize);
@@ -93,26 +87,19 @@ function drawBoard() {
if (piece && piece !== '.') {
const x = offset + c * cellSize;
const y = offset + r * cellSize;
// 棋子圆形
// 棋子圆形
ctx.beginPath();
ctx.arc(x, y, cellSize/2 - 4, 0, 2 * Math.PI);
// 根据棋子颜色设置背景
if (piece === piece.toUpperCase()) {
ctx.fillStyle = '#ffcccc'; // 红方
} else {
ctx.fillStyle = '#ccccff'; // 黑方
}
ctx.fillStyle = piece === piece.toUpperCase() ? '#FF6B6B' : '#4A90E2';
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.stroke();
// 棋子文字
// 棋子文字
ctx.font = 'bold 28px "KaiTi"';
ctx.fillStyle = '#000';
ctx.fillStyle = '#FFF';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
let char = '';
@@ -129,63 +116,80 @@ function drawBoard() {
}
}
}
// 高亮选中的棋子
// 高亮选中的棋子(金色外圈 + 准星)
if (selectedRow >= 0 && selectedCol >= 0) {
const x = offset + selectedCol * cellSize;
const y = offset + selectedRow * cellSize;
// 外发光
ctx.beginPath();
ctx.arc(offset + selectedCol * cellSize, offset + selectedRow * cellSize, cellSize/2 - 4, 0, 2 * Math.PI);
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 4;
ctx.arc(x, y, cellSize/2 - 2, 0, 2 * Math.PI);
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 6;
ctx.stroke();
// 准星(十字线)
ctx.beginPath();
ctx.moveTo(x - 15, y);
ctx.lineTo(x + 15, y);
ctx.moveTo(x, y - 15);
ctx.lineTo(x, y + 15);
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 3;
ctx.stroke();
// 中心点
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#FFD700';
ctx.fill();
}
}
// 鼠标点击事件
// 鼠标点击
canvas.addEventListener('click', (e) => {
const pos = getBoardPosition(e.clientX, e.clientY);
const row = pos.row;
const col = pos.col;
console.log('点击位置:', row, col);
console.log('我的身份:', myColor);
console.log('当前回合:', turn);
// 检查点击是否在棋盘内
if (row < 0 || row >= 10 || col < 0 || col >= 9) {
console.log('点击在棋盘外');
return;
}
// 如果是旁观者,不能走棋
if (row < 0 || row >= 10 || col < 0 || col >= 9) return;
const clickedPiece = board[row] && board[row][col];
// 旁观者
if (myColor === null) {
alert('您是旁观者,不能走棋');
if (clickedPiece && clickedPiece !== '.') {
alert('您是旁观者,只能观看');
}
return;
}
// 如果不是当前玩家回合,不能走棋
// 回合检查
if (myColor !== turn) {
alert(`现在轮到${turn === 'red' ? '红方' : '黑方'},请您等待`);
if (clickedPiece && clickedPiece !== '.') {
alert(`现在轮到${turn === 'red' ? '红方' : '黑方'},请等待`);
}
return;
}
if (selectedRow === -1) {
// 第一次点击:选棋子
const piece = board[row] && board[row][col];
if (piece && piece !== '.') {
// 检查是否选中了自己的棋子
if ((myColor === 'red' && piece === piece.toUpperCase()) ||
(myColor === 'black' && piece === piece.toLowerCase())) {
// 第一次点击:选择己方棋子
if (clickedPiece && clickedPiece !== '.') {
const isMyPiece = (myColor === 'red' && clickedPiece === clickedPiece.toUpperCase()) ||
(myColor === 'black' && clickedPiece === clickedPiece.toLowerCase());
if (isMyPiece) {
selectedRow = row;
selectedCol = col;
drawBoard();
console.log('选中棋子:', piece, '位置:', row, col);
console.log('选中:', clickedPiece, row, col);
} else {
alert('不能选择对方棋子');
alert('不能选择对方棋子');
}
}
} else {
// 第二次点击:移动棋子
console.log('尝试移动从', selectedRow, selectedCol, '', row, col);
// 第二次点击:尝试移动
console.log('移动请求:', selectedRow, selectedCol, '->', row, col);
socket.emit('move', {
room_id: roomId,
from: { row: selectedRow, col: selectedCol },
@@ -197,73 +201,57 @@ canvas.addEventListener('click', (e) => {
// Socket 事件
socket.on('connect', () => {
console.log('连接成功');
socket.emit('login', { username });
socket.emit('join_room', { room_id: roomId, password });
});
socket.on('room_joined', (data) => {
console.log('加入房间成功:', data);
// 解析棋盘
if (data.board) {
board = data.board.split('\n').map(row => row.split(''));
}
board = data.board.split('\n').map(row => row.split(''));
turn = data.turn;
myColor = data.my_color;
// 更新UI
document.getElementById('my-color').innerText = myColor ? (myColor === 'red' ? '红方' : '黑方') : '旁观';
document.getElementById('red-player').innerText = data.players.red || '等待';
document.getElementById('black-player').innerText = data.players.black || '等待';
document.getElementById('watcher-count').innerText = data.watchers;
document.getElementById('turn').innerText = turn === 'red' ? '红方' : '黑方';
// 清除选中状态
selectedRow = -1;
selectedCol = -1;
// 重新绘制
drawBoard();
console.log('我的身份:', myColor);
});
socket.on('board_updated', (data) => {
console.log('棋盘更新:', data);
board = data.board.split('\n').map(row => row.split(''));
turn = data.turn;
document.getElementById('turn').innerText = turn === 'red' ? '红方' : '黑方';
// 清除选中状态
selectedRow = -1;
selectedCol = -1;
drawBoard();
});
socket.on('player_joined', (data) => {
console.log('玩家加入:', data);
if (data.color === 'red') {
document.getElementById('red-player').innerText = data.username;
} else if (data.color === 'black') {
document.getElementById('black-player').innerText = data.username;
socket.on('move_result', (data) => {
if (data.success) {
console.log('移动成功');
selectedRow = -1; // 清除选中
drawBoard();
} else {
alert(data.message || '非法移动');
// 保持选中,但可以闪烁提示
drawBoard(); // 重绘保持高亮
}
});
socket.on('player_joined', (data) => {
if (data.color === 'red') document.getElementById('red-player').innerText = data.username;
else document.getElementById('black-player').innerText = data.username;
});
socket.on('player_left', (data) => {
console.log('玩家离开:', data);
if (data.color === 'red') {
document.getElementById('red-player').innerText = '等待';
} else if (data.color === 'black') {
document.getElementById('black-player').innerText = '等待';
}
if (data.color === 'red') document.getElementById('red-player').innerText = '等待';
else document.getElementById('black-player').innerText = '等待';
});
socket.on('error', (data) => {
console.error('错误:', data.msg);
alert(data.msg);
selectedRow = -1;
drawBoard();
});
socket.on('force_disconnect', (data) => {
@@ -272,21 +260,13 @@ socket.on('force_disconnect', (data) => {
window.location.href = '/';
});
// 复制链接
document.getElementById('copy-link').addEventListener('click', () => {
const link = window.location.href;
navigator.clipboard.writeText(link).then(() => {
alert('链接已复制到剪贴板');
}).catch(() => {
alert('复制失败,请手动复制');
});
navigator.clipboard.writeText(window.location.href).then(() => alert('链接已复制'));
});
// 添加键盘事件按ESC取消选中
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
selectedRow = -1;
selectedCol = -1;
drawBoard();
}
});