import socket
import json
import random
import string
import time

# 服务器配置
HOST = ''  # 绑定所有网卡(支持远程连接),本地测试可改为 '127.0.0.1'
PORT = 9999  # 自定义端口(避免与其他程序冲突)
BUFFER_SIZE = 1024

def generate_stimulus_sequence(n, length):
    """
    生成 N-Back 刺激序列和匹配标记列表
    :param n: N值(当前刺激与n步前比较)
    :param length: 刺激序列长度
    :return: stimulus_list(刺激序列),match_list(匹配标记,True=匹配)
    """
    stimulus_list = [random.choice(string.ascii_uppercase) for _ in range(length)]
    match_list = [False] * length

    # 从第n个索引开始,判断是否与n步前匹配
    for i in range(n, length):
        if stimulus_list[i] == stimulus_list[i - n]:
            match_list[i] = True

    return stimulus_list, match_list

def main():
    # 创建 TCP Socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 允许端口复用,避免程序重启后端口占用问题
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定地址和端口
    server_socket.bind((HOST, PORT))
    # 监听客户端连接(最大等待数5)
    server_socket.listen(5)
    print(f"服务器已启动,监听端口 {PORT},等待客户端连接...")

    # 等待客户端连接
    client_socket, client_addr = server_socket.accept()
    print(f"客户端已连接:{client_addr[0]}:{client_addr[1]}")

    try:
        # 1. 定义任务参数(可自定义修改)
        task_params = {
            "n": 2,  # N-Back的N值(2-back任务)
            "stimulus_length": 20,  # 刺激序列总长度
            "display_time": 2,  # 每个刺激呈现时间(秒)
            "prompt_time": 1  # 刺激间隔提示时间(秒)
        }
        # 发送任务参数给客户端(JSON序列化 + 编码)
        client_socket.send(json.dumps(task_params).encode('utf-8'))
        print("已发送任务参数给客户端")

        # 2. 生成刺激序列和匹配列表
        n = task_params["n"]
        stimulus_length = task_params["stimulus_length"]
        stimulus_list, match_list = generate_stimulus_sequence(n, stimulus_length)
        print("已生成刺激序列,开始执行N-Back任务...")

        # 3. 逐次发送刺激并接收客户端反馈
        result_list = []  # 存储客户端反馈结果(is_correct, reaction_time)
        for idx, stimulus in enumerate(stimulus_list):
            # 发送当前刺激给客户端
            client_socket.send(stimulus.encode('utf-8'))
            print(f"发送刺激 {idx+1}/{stimulus_length}:{stimulus}")

            # 接收客户端反馈(JSON格式)
            feedback_data = client_socket.recv(BUFFER_SIZE).decode('utf-8')
            feedback = json.loads(feedback_data)
            is_correct = (feedback["user_choice"] == 1 and match_list[idx]) or (feedback["user_choice"] == 0 and not match_list[idx])
            reaction_time = feedback["reaction_time"]
            result_list.append((is_correct, reaction_time))

            # 等待刺激间隔时间(模拟任务节奏)
            time.sleep(task_params["prompt_time"])

        # 4. 统计任务结果
        total_trials = len(result_list)
        correct_trials = sum([1 for is_correct, _ in result_list if is_correct])
        accuracy = (correct_trials / total_trials) * 100
        valid_reaction_times = [rt for _, rt in result_list if rt > 0]
        avg_reaction_time = sum(valid_reaction_times) / len(valid_reaction_times) if valid_reaction_times else 0

        # 打印服务器端统计结果
        print("\n===== 任务结束 - 服务器统计结果 =====")
        print(f"N值:{n}")
        print(f"总试次:{total_trials}")
        print(f"正确试次:{correct_trials}")
        print(f"正确率:{accuracy:.2f}%")
        print(f"平均反应时:{avg_reaction_time:.2f} 秒")

        # 5. 发送结束信号和统计结果给客户端
        end_data = {
            "task_end": True,
            "accuracy": accuracy,
            "avg_reaction_time": avg_reaction_time
        }
        client_socket.send(json.dumps(end_data).encode('utf-8'))

    except Exception as e:
        print(f"任务异常:{e}")
    finally:
        # 关闭连接
        client_socket.close()
        server_socket.close()
        print("服务器连接已关闭")

if __name__ == "__main__":
    main()

import socket
import json
import tkinter as tk
from tkinter import messagebox
import time
import threading

# 客户端配置
SERVER_PORT = 9999
BUFFER_SIZE = 1024

# 全局变量(存储任务状态和反应数据)
current_stimulus = ""
stimulus_show_time = 0.0
user_choice = -1  # -1=未输入,0=不匹配,1=匹配
input_lock = threading.Lock()

def create_gui():
    """创建tkinter图形界面"""
    root = tk.Tk()
    root.title("N-Back 客户端")
    root.geometry("400x300")  # 窗口大小

    # 刺激显示标签(大号字体)
    stimulus_label = tk.Label(root, text="等待任务开始...", font=("Arial", 48, "bold"))
    stimulus_label.pack(expand=True, pady=50)

    # 状态提示标签
    status_label = tk.Label(root, text="状态:未连接", font=("Arial", 12))
    status_label.pack(pady=10)

    # 绑定键盘事件(1=匹配,0=不匹配)
    def on_key_press(event):
        global user_choice, stimulus_show_time
        with input_lock:
            if user_choice == -1 and event.char in ["0", "1"]:
                # 计算反应时(从刺激呈现到按键的时间差)
                reaction_time = time.time() - stimulus_show_time
                user_choice = int(event.char)
                status_label.config(text=f"已输入:{user_choice} | 反应时:{reaction_time:.2f} 秒")

    root.bind("<Key>", on_key_press)

    return root, stimulus_label, status_label

def main():
    global current_stimulus, stimulus_show_time, user_choice

    # 1. 获取服务器IP(用户输入)
    server_ip = input("请输入服务器IP地址(本地测试输入127.0.0.1):").strip()
    if not server_ip:
        server_ip = "127.0.0.1"

    # 2. 创建GUI界面
    root, stimulus_label, status_label = create_gui()
    status_label.config(text=f"正在连接服务器 {server_ip}:{SERVER_PORT}...")
    root.update()  # 更新界面

    try:
        # 3. 创建TCP Socket并连接服务器
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_socket.connect((server_ip, SERVER_PORT))
        status_label.config(text=f"已连接服务器:{server_ip}:{SERVER_PORT}")
        root.update()

        # 4. 接收任务参数
        task_params_data = client_socket.recv(BUFFER_SIZE).decode('utf-8')
        task_params = json.loads(task_params_data)
        n = task_params["n"]
        stimulus_length = task_params["stimulus_length"]
        display_time = task_params["display_time"]
        prompt_time = task_params["prompt_time"]
        print(f"接收任务参数:N={n},刺激长度={stimulus_length},呈现时间={display_time}秒")
        status_label.config(text=f"任务参数:{n}-Back | 总试次:{stimulus_length}")
        root.update()
        time.sleep(2)  # 提示延迟

        # 5. 逐次接收刺激并反馈
        for idx in range(stimulus_length):
            # 重置用户输入状态
            user_choice = -1
            # 接收当前刺激
            stimulus = client_socket.recv(BUFFER_SIZE).decode('utf-8')
            current_stimulus = stimulus

            # 呈现刺激并记录呈现时间
            stimulus_label.config(text=stimulus)
            status_label.config(text=f"试次 {idx+1}/{stimulus_length} | 按1=匹配,0=不匹配")
            root.update()
            stimulus_show_time = time.time()

            # 等待用户输入或刺激呈现超时
            start_wait = time.time()
            while user_choice == -1:
                if time.time() - start_wait > display_time:
                    status_label.config(text="超时未输入!")
                    root.update()
                    break
                root.update_idletasks()
                root.after(10)  # 避免界面卡死

            # 构造反馈数据
            reaction_time = time.time() - stimulus_show_time if user_choice != -1 else 0
            feedback = {
                "user_choice": user_choice,
                "reaction_time": reaction_time
            }
            # 发送反馈给服务器
            client_socket.send(json.dumps(feedback).encode('utf-8'))

            # 刺激间隔时间
            stimulus_label.config(text="")
            root.update()
            time.sleep(prompt_time)

        # 6. 接收服务器统计结果并显示
        end_data = client_socket.recv(BUFFER_SIZE).decode('utf-8')
        end_result = json.loads(end_data)
        if end_result["task_end"]:
            accuracy = end_result["accuracy"]
            avg_reaction_time = end_result["avg_reaction_time"]
            # 显示任务结果
            result_text = f"===== 任务结束 =====\nN值:{n}\n总试次:{stimulus_length}\n正确率:{accuracy:.2f}%\n平均反应时:{avg_reaction_time:.2f} 秒"
            stimulus_label.config(text="任务结束")
            status_label.config(text=result_text)
            messagebox.showinfo("任务结果", result_text)
            root.update()

    except ConnectionRefusedError:
        messagebox.showerror("错误", "无法连接服务器,请确认服务器已启动且IP正确")
        status_label.config(text="连接失败!")
    except Exception as e:
        messagebox.showerror("错误", f"客户端异常:{e}")
    finally:
        # 关闭连接和GUI
        try:
            client_socket.close()
        except:
            pass
        root.mainloop()  # 保持GUI窗口显示

if __name__ == "__main__":
    main()