科目三 - 灯光模拟练习小程序
原创约 2302 字
写在之前
今天是五一, 大后天就要科三上车练习了. 这几天了解了下科三的考察内容. 知道科三里面第一项就是
夜间灯光模拟, 而且需要在五秒内做出应答操作. 作为一个奔三的汉子,就担心五秒反应不过来.
那就多练呗.
在 驾考宝典 上倒是有灯光模拟练习, 但是得收费才能练习. 好吧, 我心疼钱了. 趁着自己最近一直在休息, 就自己写了个小程序自己在家练反应吧.
使用方式
- 当控制台打印 --- 嘀 --- 字符时, 开始答题;
- 共需输入三个数字, 以空格分割;
- 第一个数字代表 灯光总开关, 输入 1 代表顺时针拨动一下. 输入 -1 代表逆时针拨动一下;
- 第二个数字代表 左拨杆开关, 输入 1 代表向近怀方向拨动一下. 输入 -1 代表向远怀方向拨动一下;
- 第三个数字代表 双闪灯开关, 输入 1 代表按动 双闪一次, 以此类推;
- 输入完成后, 单击回车提交答案. 等待评判和下一题;
注意事项
- 一共会随机生成六道题. 第一道恒等于 "开启前照灯", 最后一道题恒等于 "关闭所有灯光,开始其他考试";
- 使用过程中. 程序会记录上一题操作后所有灯光的开启状态. 所以, 每一题的操作步骤均与上一题后的灯光位置强相关;
- 就像上一条说的. 比如上一题是开启远光灯, 当前题需要远近交替. 此时需要至少拨动5次拨杆才能判定打光正确;
- 需要远近光灯交替时, 需要向近怀方向拨动四次;
- 所有操作必须在5秒内完成, 否则会超时退出考试;
- "嘀"声在出题1秒后出现, 弥补打印比口播快的客观事实;
运行环境
代码片段
package com.github.guocay.util;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.stream.IntStream;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* 科目三 灯光模拟
* @author aCay
* @since 2023.05.01
*/
public class LightingSimulation {
/**
* 程序中用到了 System.in 对象. 所以必须是单例模式
*/
public static final LightingSimulation INSTANCE;
/**
* 日志对象和一些常量
*/
private static final Logger logger = Logger.getLogger(LightingSimulation.class.getName());
private static final String RUN_TIME_OUT = "java.util.concurrent.TimeoutException";
private static final String RUN_EXECUTION = "java.util.concurrent.ExecutionException";
/**
* 配置信息
*/
private static final int CONFIG_MAX_TOPIC_TIMEOUT = 5;
private static final int CONFIG_MAX_ALTERNATE_COUNT = 4;
private static final int CONFIG_TOTAL_TOPIC = 5;
/**
* 拨杆状态 枚举
*/
private static final int LEVER_NEAR_LIGHT_STATUS = 0;
private static final int LEVER_PARALLEL_LIGHT_STATUS = -1;
private static final int LEVER_ALTERNATE_LIGHT_STATUS = CONFIG_MAX_ALTERNATE_COUNT;
/**
* 总控状态 枚举
*/
private static final int MASTER_CLOSE_LIGHT_STATUS = 0;
private static final int MASTER_CONTOUR_LIGHT_STATUS = 1;
private static final int MASTER_FRONT_LIGHT_STATUS = 2;
/**
* 创建线程池
*/
private static final ExecutorService THREAD_POOL;
/**
* 拉取用户输入答案的线程
*/
private static final Callable<Operation> PULL_ANSWER;
/**
* 灯光模拟考试题库及答案
*/
@SuppressWarnings("unchecked")
private static final Entry<String, LightStatus>[] QUESTIONS = new Entry[10];
/**
* 随机数生成器
*/
private final Random random;
/**
* 计时器
*/
private final StopWatch stopWatch;
/**
* 记录车辆当前灯光状态
*/
private final LightStatus currentLightStatus;
static {
LightStatus nearLightStatus = new LightStatus(MASTER_FRONT_LIGHT_STATUS, LEVER_NEAR_LIGHT_STATUS, false);
LightStatus alternateLightStatus = new LightStatus(MASTER_FRONT_LIGHT_STATUS, LEVER_ALTERNATE_LIGHT_STATUS, false);
LightStatus parallelLightStatus = new LightStatus(MASTER_FRONT_LIGHT_STATUS, LEVER_PARALLEL_LIGHT_STATUS, false);
LightStatus closeLightStatus = new LightStatus(MASTER_CLOSE_LIGHT_STATUS, LEVER_NEAR_LIGHT_STATUS, false);
LightStatus emergencyLightStatus = new LightStatus(MASTER_CONTOUR_LIGHT_STATUS, LEVER_NEAR_LIGHT_STATUS, true);
// 设置题库及答案
QUESTIONS[0] = new SimpleImmutableEntry<>("请开启前照灯", nearLightStatus);
QUESTIONS[1] = new SimpleImmutableEntry<>("同方向近距离跟车驾驶", nearLightStatus);
QUESTIONS[2] = new SimpleImmutableEntry<>("通过有交通信号灯控制的路口", nearLightStatus);
QUESTIONS[3] = new SimpleImmutableEntry<>("夜间在窄路窄桥与非机动车会车", nearLightStatus);
QUESTIONS[4] = new SimpleImmutableEntry<>("夜间在有路灯照明良好的道路上行驶", nearLightStatus);
QUESTIONS[5] = new SimpleImmutableEntry<>("夜间通过没有交通信号灯控制的路口", alternateLightStatus);
QUESTIONS[6] = new SimpleImmutableEntry<>("夜间超越前方车辆", alternateLightStatus);
QUESTIONS[7] = new SimpleImmutableEntry<>("夜间在没有路灯或照明条件不良条件下行驶", parallelLightStatus);
QUESTIONS[8] = new SimpleImmutableEntry<>("路边临时停车", emergencyLightStatus);
QUESTIONS[9] = new SimpleImmutableEntry<>("关闭所有灯光,开始其他考试", closeLightStatus);
// 设置拉取答案的线程和线程池
THREAD_POOL = Executors.newFixedThreadPool(1);
final Scanner SCANNER = new Scanner(System.in);
PULL_ANSWER = () -> Operation.of(SCANNER.nextLine());
// 设置单例模式下的实例
INSTANCE = new LightingSimulation();
}
private LightingSimulation(){
this.random = new Random();
this.stopWatch = new StopWatch();
this.currentLightStatus = QUESTIONS[QUESTIONS.length - 1].getValue();
}
/**
* 启动灯光模拟考试
*/
public void startup(){
// 加 1 是为了将 "关闭所有灯光" 算进去.
startup(CONFIG_TOTAL_TOPIC + 1, CONFIG_MAX_TOPIC_TIMEOUT);
}
/**
* 启动灯光模拟考试
* @param topicTotal 总出题数
* @param topicTimeout 超时时间
*/
public void startup(int topicTotal, int topicTimeout){
// 打印准备信息
printPrepareMessage();
// 循环出六道题
IntStream.range(0, topicTotal).forEach(index -> {
// 通过 Future 的超时异常, 实现五秒内答题.
try {
// 从题库中抽取考题后,打印题目. 并返回期望答案.
LightStatus els = printQuestions(index);
// 1. 提交任务, 并指定拉取的最大时间; 2. 将操作追加至当前状态;
appendOperation(THREAD_POOL.submit(PULL_ANSWER).get(topicTimeout, TimeUnit.SECONDS));
// 校验状态是否符合要求
judgmentStatus(els);
}catch (Exception ex){
String errorInfo = switch (ex.getClass().getName()){
case RUN_TIME_OUT -> "答题超时, 成绩不合格!!!";
case RUN_EXECUTION -> "输入值非法, 成绩不合格!!!";
default -> "发生未预期错误, 成绩不合格!!!";
};
applicationExit(errorInfo);
}
// 准备下一轮, 回退远近光交替至近光.
prepareNextRound();
});
logger.info("考试结束, 成绩合格!!!");
THREAD_POOL.shutdownNow();
}
/**
* 打印准备信息
*/
private void printPrepareMessage() {
logger.info("""
答题时需要输入三个数字,并以空格分开.
`1. 第一个数字代表灯光总开关拨动次数, 输入 1 代表顺时针拨动一下, -1 代表逆时针拨动一下.
`2. 第二个数字代表左拨杆拨动次数. 输入 1 代表向近怀拨动一下, -1 代表向远怀拨动一下.
`3. 第三个数字代表双闪控制开关, 输入正整数. 表示按动几次.
输入完毕后,必须按回车键提交.
"""
);
logger.info("""
下面将开始模拟夜间灯光的考试!!!
请在嘀声后五秒内完成答题.
"""
);
}
/**
* 打印当前的灯光状态
*/
private void printCurrentStatus() {
// 获取总灯光状态
String master = switch (currentLightStatus.master){
case MASTER_CLOSE_LIGHT_STATUS -> "关闭";
case MASTER_CONTOUR_LIGHT_STATUS -> "示廓灯";
default -> "前照灯";
};
// 获取左拨杆状态
String lever = switch (currentLightStatus.lever){
case LEVER_PARALLEL_LIGHT_STATUS -> "远光灯";
case LEVER_NEAR_LIGHT_STATUS -> currentLightStatus.master == MASTER_CLOSE_LIGHT_STATUS ? "关闭" : "近光灯";
default -> "远近光交替";
};
// 获取双闪灯状态
String emergency = currentLightStatus.emergency ? "开启" : "关闭";
System.err.printf("""
操作耗时: %d毫秒.
操作后的灯: 总控[%s], 拨杆[%s], 双闪[%s].
""",
stopWatch.end(), master, lever, emergency
);
}
/**
* 准备下一轮, 回退远近光交替至近光.
*/
private void prepareNextRound() {
currentLightStatus.lever = min(LEVER_NEAR_LIGHT_STATUS, currentLightStatus.lever);
}
/**
* 程序退出, 打印提示语
* @param msg 提示信息
*/
private void applicationExit(String msg) {
logger.severe(msg);
THREAD_POOL.shutdownNow();
System.exit(0);
}
/**
* 校验状态是否符合要求
* @param expectationLightStatus 期望灯光状态
*/
private void judgmentStatus(LightStatus expectationLightStatus) {
if (!currentLightStatus.equals(expectationLightStatus)){
applicationExit("答案错误, 成绩不合格!!!");
}
}
/**
* 将操作追加至当前状态
* @param operation 动作
*/
private void appendOperation(Operation operation) {
currentLightStatus.setMaster(operation.master);
currentLightStatus.setLever(operation.lever);
currentLightStatus.setEmergency(operation.emergency);
// 打印当前灯光状态
printCurrentStatus();
}
/**
* 打印题目并 返回期望的目标灯光
* @param index 第几题
* @return 期望的目标灯光
*/
private LightStatus printQuestions(int index) throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
Entry<String, LightStatus> question = (switch (index){
case 0 -> QUESTIONS[0];
case CONFIG_TOTAL_TOPIC -> QUESTIONS[QUESTIONS.length - 1];
default -> {
Entry<String, LightStatus> _temp = null;
while (_temp == null){
// 加 1 是为了 错开下标为 0 的题.
int num = random.nextInt(7) + 1;
_temp = QUESTIONS[num];
QUESTIONS[num] = null;
}
yield _temp;
}
});
logger.info(question.getKey());
TimeUnit.SECONDS.sleep(1);
System.err.println("--- 嘀 ---");
stopWatch.begin();
return question.getValue();
}
/**
* 操作内容, 答卷
*/
private record Operation(int master, int lever, int emergency) {
/**
* 解析输入, 生成操作
* @param input 输入字符
* @return 操作
*/
public static Operation of(String input) {
String[] info = input.split(" ");
return new Operation(Integer.parseInt(info[0]), Integer.parseInt(info[1]), Integer.parseInt(info[2]));
}
}
/**
* 灯光状态
*/
private static class LightStatus {
/**
* 总开关
* <li>0: 代表关闭总开关</li>
* <li>1: 代表开启示廓灯</li>
* <li>2: 代表开启前照灯</li>
*/
private int master;
/**
* 左拨杆
* <li>-1: 代表开启远光灯</li>
* <li>0: 代表开启近光灯</li>
* <li>1: 开启远近交替</li>
*/
private int lever;
/**
* 双闪灯
* <li>true: 开启双闪灯</li>
* <li>false: 关闭双闪灯</li>
*/
private boolean emergency;
private LightStatus(int master, int lever, boolean emergency) {
this.master = master;
this.lever = lever;
this.emergency = emergency;
}
public void setMaster(int master) {
// 如果操作后的结果大于2, 则回退至2.
// 如果操作后的结果小于0, 则回退至0.
this.master = max(min(this.master + master, MASTER_FRONT_LIGHT_STATUS), MASTER_CLOSE_LIGHT_STATUS);
}
public void setLever(int lever) {
// 如果操作后的结果小于 -1, 则回退至 -1.
// 如果操作后的结果大于 4, 则回退至 4.
this.lever = max(min(this.lever + lever, CONFIG_MAX_ALTERNATE_COUNT), LEVER_PARALLEL_LIGHT_STATUS);
}
public void setEmergency(int emergency) {
// 判断操作的次数是奇数还是偶数, 是奇数则不改变灯光状态. 反之则改变状态.
this.emergency = (emergency % 2 == 0) == this.emergency;
}
@Override
public boolean equals(Object o) {
return this == o ||
(o instanceof LightStatus that
&& master == that.master
&& emergency == that.emergency
&& lever == that.lever);
}
}
/**
* 计时器
*/
private static class StopWatch {
/**
* 记录当前时间
*/
private Long currentTimeMillis;
/**
* 计时器开始工作.
*/
public void begin(){
this.currentTimeMillis = System.currentTimeMillis();
}
/**
* 计数器结束工作, 并返回持续时间.
* @return 返回时间间隔
*/
public Long end(){
return currentTimeMillis == null ? null : System.currentTimeMillis() - currentTimeMillis;
}
}
public static void main(String[] args) {
INSTANCE.startup();
// INSTANCE.startup(5, 5);
}
}