范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

用原生JS开发编程游戏机器人流水线

  作者:吴亮(月影)
  记得之前玩过一个 flash 编程小游戏,印象深刻,叫"机器人流水线(manufactoria)",不知道有没有同学也玩过。可惜的是,现在 falsh 已经停止运行了,这个原版的小游戏无法体验到。
  不过最近几天,我凭着之前的印象,复刻出了这个小游戏。
  这个小游戏的规则是,将左侧的元件放置到右侧的面板上,然后点击运行,机器人会沿着元件指定的路径运行,并影响地步序列的状态,最终按照任务的要求完成,即可过关。
  例如上面的截图是第五关,任务是"队列里不能出现不同颜色的球",也就是说如果队列中只有红球或只有蓝球,要把机器人移动到 处,否则将机器人移到任意其他空格。
  我们能将元件放置到在任意白色空格处,机器人走到元件上会根据元件的类型来产生相应的动作。
  manufactoria 的元件非常简单,只有两种类型:传送器和比较器,但根据不同的作用一共分为 7 种:
  其中传送器有五种,四种带颜色的,机器人通过的时候会将对应颜色的球添加到序列的末尾,还有最后一种黑色的,机器人通过,序列不变。
  比较器有两种,分别是红蓝比较器和黄绿比较器。比较器的作用是,当机器人通过它时,判断序列头部的球颜色,若颜色是比较器允许的颜色,则机器人朝对应的加号方向前进,并将该序列头部的这个球取出,否则,机器人沿着弧形箭头方向前进,且序列保持不变。
  神奇的是有了这些简单的元件,我们就可以让机器人完成复杂的任务了。而且这和编程思想是一致的,我们可以通过元件构建出顺序,选择和循环结构体!
  如下图,在第 22 关,可以用绿色小球构建出循环体解决问题:
  好了,前面说了规则,有兴趣的同学可以自行挑战,目前有 20 多个关卡,我会不定期更新新的关卡,等待大家的挑战。
  接下来,我们看一下游戏是怎么实现的。
  首先是面板的 HTML 结构,这个结构非常简单:                                                                                    说明:鼠标选择上方元件添加到右侧面板中,键盘上下左右旋转,空格翻转。                                                  序列 ← ❤️     结果 →
  在这里我就不多说了,元件是通过 CSS 样式绘制的,比如比较器: .comparator {   margin: 10px 20px;   border-bottom-right-radius: 50%;   border-bottom-left-radius: 50%; } .comparator::before {   content: "+";   margin-left: -10px; } .comparator::after {   content: "+";   margin-left: 10px; }  .comparator.red::before {   color: red; } .comparator.green::before {   color: green; }  .comparator.blue::after {   color: blue; } .comparator.yellow::after {   color: orange; }
  因为所有的元件结构都不复杂,所以用一个 HTML 标签,加上 before 和 after 伪元素,就完全可以绘制出来的。
  右侧的网格是一个 grid 布局的棋盘: #app {   width: 520px;   height: 520px;   border-bottom: solid 1px #0002;   border-right: solid 1px #0002;   background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.15) 2.5%, transparent 2.5%), linear-gradient( rgba(0, 0, 0, 0.15) 2.5%, transparent 2.5%);   background-size: 40px 40px;   background-repeat: repeat;   display: grid;   grid-template-columns: repeat(13, 40px);   grid-template-rows: repeat(13, 40px); }  #app > p {   text-align: center;   font-size: 1.8rem;   line-height: 48px;; }
  在网格中添加对应的元件,就只要找到对应的格子往里添加指定类型的元素就可以了。
  机器人是绝对定位的元素,它移动的时候的动画效果可以通过 transition 给出: #robot {   position: absolute;   transition: all linear .2s; }  #robot::after {   font-size: 1.8rem;   content: "";   margin: 5px; }
  这样,基本的 HTML 和 CSS 就实现完成了。实际上,大部分 UI 和交互效果都可以通过 HTML 和 CSS 指定,让 JS 只需要负责控制逻辑,这样就简单很多。
  接下来我们看具体的逻辑。
  首先我们实现一个点击左侧面板的元件,将元件用鼠标拾取的效果: unction enablePicker() {   const buttons = panel.querySelector(".buttons");   buttons.addEventListener("mousedown", ({target}) => {     if(main.className !== "running" && target !== buttons && target.className) {       const node = target.cloneNode(true);       mousePick.innerHTML = "";       mousePick.appendChild(node);     }   });   window.addEventListener("mousemove", ({x, y}) => {     mousePick.style.left = `${x - 25}px`;     mousePick.style.top = `${y - 25}px`;   });   window.addEventListener("contextmenu", (e) => {     e.preventDefault();     return false;   });   window.addEventListener("mouseup", ({target}) => {     if(target.parentNode !== buttons && target.className !== "normal") {       mousePick.innerHTML = "";     }   });   window.addEventListener("keydown", ({key}) => {     const el = mousePick.children[0];     if(!el || el.className === "trash") return;     if(key === "ArrowRight") {       el.dataset.turn = 0;     } else if(key === "ArrowDown") {       el.dataset.turn = 1;     } else if(key === "ArrowLeft") {       el.dataset.turn = 2;     } else if(key === "ArrowUp") {       el.dataset.turn = 3;     } else if(key === " ") {       let n = Number(el.dataset.flip) || 0;       el.dataset.flip = ++n % 2;     }     if(key.startsWith("Arrow") && el.classList.contains("comparator")) {       el.dataset.turn = (Number(el.dataset.turn) + 3) % 4;     }   }); }
  这里,我们直接用 cloneNode,将面板上的元素复制出来,做出一个透明效果,跟随鼠标移动。另外,我们还做了键盘控制,通过键盘控制元件的具体方向:
  注意,我们用 JS 控制元素方向的时候,通过设置 turn 和 flip 来表示元素翻转,至于元素具体的展现,则通过 CSS 来定义: *[data-turn="1"] {   transform: rotate(.25turn); } *[data-turn="2"] {   transform: rotate(.5turn); } *[data-turn="3"] {   transform: rotate(.75turn); }  *[data-flip="1"] {   transform: scale(-1, 1); } *[data-turn="1"][data-flip="1"] {   transform: rotate(.25turn) scale(-1, 1); } *[data-turn="2"][data-flip="1"] {   transform: rotate(.5turn) scale(-1, 1); } *[data-turn="3"][data-flip="1"] {   transform: rotate(.75turn) scale(-1, 1); }
  接着是设置和移动机器人的函数: function setRobot() {   const start = app.querySelector(".start");   const row = Number(start.dataset.x);   const col = Number(start.dataset.y);   let {x, y} = app.getBoundingClientRect();   x = x + col * 40;   y = y + row * 40;   const el = document.getElementById("robot") || document.createElement("p");   el.id = "robot";   el.style.left = `${x}px`;   el.style.top = `${y}px`;   el.dataset.x = x;   el.dataset.y = y;   el.dataset.row = row;   el.dataset.col = col;   el.dataset.fromDirection = "";   document.body.appendChild(el); }  function moveRobot(direction) {   let x = Number(robot.dataset.x);   let y = Number(robot.dataset.y);   let row = Number(robot.dataset.row);   let col = Number(robot.dataset.col);   let fromDirection = "";   if(direction === "left") {     x -= 40;     col--;     fromDirection = "right";   } else if(direction === "right") {     x += 40;     col++;     fromDirection = "left";   } else if(direction === "up") {     y -= 40;     row--;     fromDirection = "down";   } else if(direction === "down") {     y += 40;     row++;     fromDirection = "up";   }   robot.style.left = `${x}px`;   robot.style.top = `${y}px`;   robot.dataset.x = x;   robot.dataset.y = y;   robot.dataset.row = row;   robot.dataset.col = col;   robot.dataset.fromDirection = fromDirection;   // console.log(row, col, robot);    return new Promise(resolve => {     robot.addEventListener("transitionend", () => {       // console.log(row, col, robot.dataset.row, robot.dataset.col);       resolve(robot);     }, {once: true});     // 防止浏览器transitionend事件有时候不被触发     setTimeout(() => resolve(robot), 220);   }); }
  这里, setRobot  将机器人设置到起始位置,起始位置在网格中是一个 className 包含 start 的 p 元素,这个元素的位置在后续调用 loadLevel 读取当前关卡的时候初始化。
  moveRobot  实际上是一个异步方法,它返回一个 Promise,在机器人执行完动作之后 resolve。不过这里有个细节要注意,我一开始使用transitionend  来判断动画结束,但是浏览器不能保证transitionend  每次都被触发,所以有时候机器人会不明原因停下来,后来我就加了一个 setTimeout 来防止这种情况。
  接下来的一系列方法和底部序列有关,序列代表着输入输出,机器人就是通过移动来影响序列,从而达成指定任务。序列实际上是一个队列,操作比较简单。 function setDataList(list = []) {   io.innerHTML = "序列 ← ";   for(let i = 0; i < list.length; i++) {     const el = document.createElement("i");     el.innerHTML = list[i];     io.appendChild(el);   } }  function getTopData() {   const item = io.querySelector("i");   if(item) return item.innerHTML;   else return null; }  function popData() {   const item = io.querySelector("i");   item.style.width = 0;   return new Promise(resolve => {     item.addEventListener("transitionend", () => {       item.remove();       resolve(item);     }, {once: true});     // 防止浏览器transitionend事件有时候不被触发     setTimeout(() => {       item.remove();       resolve(item);     }, 220);   }); }  function appendData(data = "") {   const el = document.createElement("i");   el.innerHTML = data;   io.appendChild(el); }  function getIOData() {   const list = io.querySelectorAll("i");   let ret = "";   for(let i = 0; i < list.length; i++) {     ret += list[i].innerHTML;   }   return ret; }
  然后是一个辅助方法,用来获得机器人所在位置的棋盘元素。我们在初始化棋盘的时候,会给每个元素设置 x 和 y 坐标,在机器人走动的时候,也会更新对应的 row 和 col 坐标,所以我们通过选择器就可以快速找到机器人所在位置的棋盘格子,从而判断其中的元件。 function getRobotCell() {   let x = Number(robot.dataset.row);   let y = Number(robot.dataset.col);   const cell = document.querySelector(`#app > p[data-x="${x}"][data-y="${y}"]`);   return cell; }
  接下来就是代码最核心的部分了。 function checkCell(cell, fromDirection) {   const ret = {     direction: null,     effect: null,     type: null,     data: false,   };    const children = cell.children;   if(children.length) {     for(let i = 0; i < children.length; i++) {       const el = children[i];       const flip = el.dataset.flip;       const turn = el.dataset.turn;       if(el.classList.contains("pass")) {         ret.type = "pass";         // 通道         if(children.length > 1) {           // 交叉通道           if(fromDirection === "up" || fromDirection === "down") {             if(turn === "0" || turn === "2") continue;           }           if(fromDirection === "left" || fromDirection === "right") {             if(turn === "1" || turn === "3") continue;           }         }         if(turn === "0") ret.direction = "right";         if(turn === "1") ret.direction = "down";         if(turn === "2") ret.direction = "left";         if(turn === "3") ret.direction = "up";         if(el.classList.contains("red")) ret.effect = "";         if(el.classList.contains("green")) ret.effect = "";         if(el.classList.contains("yellow")) ret.effect = "";         if(el.classList.contains("blue")) ret.effect = "";       } else if(el.classList.contains("comparator")) {         // 比较器         ret.type = "comparator";         const data = getTopData();         if(data === "" && el.classList.contains("red")) {           if(turn === "0") ret.direction = "left";           if(turn === "1") ret.direction = "up";           if(turn === "2") ret.direction = "right";           if(turn === "3") ret.direction = "down";           ret.data = true;         } else if(data === "" && el.classList.contains("green")) {           if(turn === "0") ret.direction = "left";           if(turn === "1") ret.direction = "up";           if(turn === "2") ret.direction = "right";           if(turn === "3") ret.direction = "down";           ret.data = true;         } else if(data === "" && el.classList.contains("blue")) {           if(turn === "0") ret.direction = "right";           if(turn === "1") ret.direction = "down";           if(turn === "2") ret.direction = "left";           if(turn === "3") ret.direction = "up";           ret.data = true;         } else if(data === "" && el.classList.contains("yellow")) {           if(turn === "0") ret.direction = "right";           if(turn === "1") ret.direction = "down";           if(turn === "2") ret.direction = "left";           if(turn === "3") ret.direction = "up";           ret.data = true;         } else {           if(turn === "0") ret.direction = "down";           if(turn === "1") ret.direction = "left";           if(turn === "2") ret.direction = "up";           if(turn === "3") ret.direction = "right";         }       }       if(flip === "1") {         // 翻转交换         if(turn === "0" || turn === "2") {           if(ret.direction === "left") ret.direction = "right";           else if(ret.direction === "right") ret.direction = "left";         } else {           if(ret.direction === "up") ret.direction = "down";           else if(ret.direction === "down") ret.direction = "up";         }       }     }   }   // console.log(ret);   return ret; }  function checkState() {   const cell = getRobotCell();   const fromDirection = robot.dataset.fromDirection;   let state = {     direction: null,     effect: null,     accepted: false,     fromDirection,   };   if(cell.className === "flag") {     state.accepted = true;   } else if(cell.className !== "start") {     state = {       ...state,       ...checkCell(cell, fromDirection),     };   }   return state; }
  当机器人移动到一个格子的时候,我们通过 checkState 判断他的状态,状态包括四个信息,direction:机器人当前可以移动的方向,effect:机器人操作序列的动作,accepted:机器人是否移动到 ,fromDirection:机器人上一步从哪里移动过来的。
  checkCell 则是具体的判断逻辑,我们通过格子中的元件来具体判断机器人的这些状态,这部分逻辑虽然较繁琐,但其实也不太复杂,唯一需要注意的是,一个网格中可以放两个相互垂直的传送器,当机器人经过的时候,如果有两个方向,会默认选择直行的方向,这也是为什么我们需要 fromDirection 来判断机器人从哪个方向过来。
  接下来是展示结果,运行、停止按钮状态,sleep 等细节,就不一一赘述了。 function initResult() {   result.innerHTML = "结果 →"; }  function appendResult(success = false) {   const r = success ? "A" : "E";   const el = document.createElement("span");   el.innerHTML = r;   if(success) el.className = "accept";   result.appendChild(el); }  function sleep(ms = 10) {   return new Promise(resolve => {     setTimeout(resolve, ms);   }); }  runBtn.addEventListener("mousedown", async () => {   mousePick.innerHTML = "";   runBtn.className = "btn tap";   runBtn.disabled = true;   main.className = "running";   await run(); }); stopBtn.addEventListener("mousedown", () => {   mousePick.innerHTML = "";   stopBtn.className = "btn tap";   main.className = "";   // setRobot(); }); window.addEventListener("mouseup", () => {   if(stopBtn.className === "btn tap") {     stopBtn.className = "btn";     // runBtn.disabled = false;     // runBtn.className = "btn";   } });
  然后,我们根据关卡数据,读取和初始化对应的关卡: let currentLevel; function loadLevel(level) {   const data = levels[level];   currentLevel = {     ...data,     level,   };   taskInfo.innerHTML = `

任务:${data.task}

提示:${data.hint}`; const items = document.querySelectorAll(".buttons > p"); for(let i = 0; i < items.length; i++) { if(!data.units.includes(i)) { items[i].classList.add("hide"); } else { items[i].classList.remove("hide"); } } setDataList([...data.tests[0].data]); const board = new Array(169); board.fill(-1); const size = data.size || 13; const v = (13 - size) / 2; const range = [ v, v, v + size, v + size, ]; const [a, b, c, d] = range; for(let i = a; i < c; i++) { for(let j = b; j < d; j++) { const idx = i * 13 + j; board[idx] = 0; } } const s = v + Math.floor(size / 2); const start = v * 13 + s; const end = (v + size - 1) * 13 + s; board[start] = 1; board[end] = 2; init(board); const savedData = localStorage.getItem(`manufactoria-level-${level}`); if(savedData) { const data = JSON.parse(savedData); for(let i = 0; i < data.cells.length; i++) { const cell = data.cells[i]; const el = document.createElement("p"); el.className = cell.state; el.dataset.turn = cell.turn; el.dataset.flip = cell.flip; app.children[cell.idx].appendChild(el); } } setRobot(); return currentLevel; }   初始化之后,当放置好元件,点击运行时,让机器人运行起来: async function run() { levelPicker.disabled = true; const tests = currentLevel.tests; initResult(); for(let i = 0; i < tests.length; i++) { const {data, accept} = tests[i]; setDataList([...data]); setRobot(); await sleep(); await moveRobot("down"); while(true) { if(main.className !== "running") break; const state = checkState(); if(state.direction) { if(state.type === "comparator" && state.data) { await Promise.all([ moveRobot(state.direction), popData(), ]); } else { await moveRobot(state.direction); if(state.effect) { appendData(state.effect); } } } else { break; } } if(main.className !== "running") break; const cell = getRobotCell(); if(accept === true) { appendResult(cell.className === "flag"); } else if(typeof accept === "string") { if(cell.className !== "flag") { appendResult(false); } else { appendResult(accept === getIOData()); } } else { appendResult(cell.className !== "flag"); } await sleep(500); } runBtn.className = "btn"; runBtn.disabled = false; if(main.className === "running") { const success = !result.textContent.includes("E"); const el = document.createElement("span"); el.innerHTML = success? ":成功":":失败"; if(success) el.className = "accept"; result.appendChild(el); setDataList([]); } main.className = ""; levelPicker.disabled = false; setRobot(); }   因为有的关卡比较复杂,玩家也不希望好不容易通关的结果,下一次进游戏又没有了,所以我们做一个 localStorage 的本地保存机制: // 把数据保存到本地 function saveLevel() { const {level} = currentLevel; const data = {level, cells: []}; const cells = app.children; for(let i = 0; i < 169; i++) { const cell = cells[i]; if(cell.children.length) { for(let j = 0; j < cell.children.length; j++) { const item = cell.children[j]; const d = { state: item.className, turn: item.dataset.turn, flip: item.dataset.flip, idx: Number(cell.dataset.x) * 13 + Number(cell.dataset.y), }; data.cells.push(d); } } } localStorage.setItem(`manufactoria-level-${level}`, JSON.stringify(data)); }   最后的最后,我们做一个下拉列表来选择对应的关卡: function initLevelPicker() { const len = levels.length; levelPicker.innerHTML = ""; for(let i = 0; i < len; i++) { const option = new Option(i + 1, i); levelPicker.appendChild(option); } levelPicker.addEventListener("change", () => { loadLevel(levelPicker.value); }); loadLevel(levelPicker.value); } initLevelPicker();   这样,我们的游戏就开发完成了。实际上这个游戏本身开发的难度并不高,但是玩法却很丰富,关卡也很有挑战性。这就是编程游戏的乐趣。   有同学玩通关的话,欢迎戳下方链接,在代码评论区交流玩法心得~   manufactoria - 码上掘金 manufactoria - 码上掘金   关注「字节前端 ByteFE」公众号,追更不迷路!


沃尔沃官方试驾活动之深圳站回顾10月30日有幸参加了沃尔沃官方试驾活动驾域深圳站,由深标嘉4S店协办。白嫖了一整天的活动吃喝,不搞篇回顾都不好意思了。试驾地点选择在美丽的大鹏半岛上的佳兆业万豪酒店,考虑到单程距我和我的父亲记忆录点滴生活272今天是11月3日,身处外地一个人坐在街边悼念父亲。刚刚走过一个老年旅行团,我是真羡慕他们的生活,充分享受了国家发展的红利,物质丰富,生活多姿多彩。前些年父母还生活在老家的时候,我因山西翼城银杏秋色惹人醉又是一年银杏季,山西翼城,深秋的古城村银杏黄如约登场。触目所及满眼金色,伴随飒飒秋风,置身其中,一棵棵银杏树在阳光的照耀下金黄灿烂,一切都如同走进童话世界一般那么美好那么惬意。据悉闽清白岩山,被遗忘的八闽岳祖自从二年前与朋友一起游玩了闽清白岩山之后,一直都想着再找个机会与朋友们重游,不仅因为深秋的白岩山特别美,还因为白岩山目前尚未被完全开发,仍保存着一份自然古朴的美,可以让人感受到那份山东省两家4A级旅游景区被取消旅游景区质量等级新京报贝壳财经讯11月3日,新京报贝壳财经记者从山东省文化和旅游厅官方网站获悉,日前山东省文化和旅游厅发布公告,为加强旅游景区质量管理,提升旅游景区品质,净化旅游消费环境,依照中华草堂寺飘了千年的烟雾,是真的吗?春季去太平山森林公园观瀑布,曾在一水潭边见到了西域高僧鸠摩罗什的塑像。夏天去与李老师阎老师圭峰山,路过草堂寺,顺道参观了一番,专程去看传说已经飘扬了一千多年的草堂烟雾。草堂寺在西安一个颠覆你认知的神奇国度斐济人口报告(世界人口系列3)别着急,慢慢干,事情总有干完的时候,即使干不完,也没有什么大不了的这句富含哲理的名言,是斐济人的人生哲学!各位被生活和工作压得无法呼吸!是不是觉得非常羡慕这种吃饱了饭坐在美丽的海边带着翠花去远方!95后青年毕业骑行1300公里追梦,第一站就是来青岛看海半岛全媒体见习记者张超近日,云南小哥赵荣坤的骑行之旅获得了网友的关注,在从西安到青岛1300多公里的旅途中,他用视频记录下自己与翠花的成长经历。记者随后联系了这位远道而来的客人,听合江亭畔,历史与未来的交织窗含西岭千秋雪,门泊东吴万里船,唐代大诗人杜甫,曾居于成都杜甫草堂,写下这脍炙人口的千古名诗。而这诗中之地,正是成都府南河畔,两江交汇处。合江亭合江亭始建于1200多年前,垒基高数甘南,去了一次相当于有了9次旅行这里的平均海拔3000米,没有西藏那么高,但除了海拔,藏地的一切甘南都有。这里集聚藏传佛教精华,无论是闻名于世的宏大寺院,还是朴实无华的山间小庙,都沉淀着亘古不变的禅意。这里金顶耀湖南人喝酒怪象,酒桌不见当地名酒,反而是这4款酒备受追捧要说湖南,近几年可是成为大家疯狂打卡的地点之一。古时候的湖南,物产丰富山川秀美,是著名的鱼米之乡,吸引无数人前往。如今乘着网红旅游打卡的红利,湖南又进一步闻名。在湖南居住的人们,普
二战转折究竟是什么?斯大林格勒?还是库尔斯克保卫战?你认为的二战转折点究竟是什么呢?是斯大林格勒战役?还是库尔斯克保卫战?一千个读者有一千个哈姆雷特,很多人在这个问题上一直争论不休,那么什么才能称之为战略转折点?真正扭转二战的战役又不让娶老女人,好烦!笑谈清太祖努尔哈赤造反的原因历史学者如何评价努尔哈赤,我们说不清楚。今天要让努尔哈赤在故事中做一回恶人。说说努尔哈赤在明朝万历46年(公元1618年)造反的由头之五。干大事的,做什么都弄得规规矩矩,努尔哈赤准11月23日政协日历,一图速览七君子出狱后,在南京爱国老人马相伯家中合影。右起李公朴王造时马相伯沈钧儒邹韬奋史良章乃器沙千里杜重远1936年初,上海文化界率先成立救国会,提出了建立起民族统一阵线的主张。随着日本战争剧主题,一口气看完14集差点错过的,演员也有不错的演技历史长河中,有多少无名英雄的故事我们不知道?而我们今天所拥有的一切安宁和幸福,都是他们用生命和鲜血换来的。比如参加过松茂岭保卫战的年轻红军战士,还有闽西的英雄儿女。1934年9月218岁女孩嫁非洲总统,生下两女后却仓皇而逃1968年,年仅18岁的台湾女服务员林碧春摇身一变成为非州总统夫人,享尽荣华富贵备受宠爱,然而婚后没多久,林碧春就频频向大使馆求助,希望离开非州回到台湾。究竟发生了什么,让这样一位石奶引因长的太美被印在1元纸币上,如今她过的怎么样了?文梦琴编辑世界16岁那年,明媚动人的少女被一个画家拦住,为她画了一副自画像。11年后,全中国人都看见了她的画像,她出现在了1元纸币上,这样火遍全中国给她带来了什么影响?几十年过去了14岁!他是中国历史上最残酷最奇葩最变态的小皇帝,没有之一公元465年,南北朝时期刘宋朝第六位皇帝刘子业因作恶多端被人杀死,他的叔父刘彧被拥立为帝。刘彧当上皇帝后并没有励精图治。为了巩固皇位,刘彧将十多个侄儿们全部赐死。他还继承了刘子业的古代太监为何敢骚扰皇后?在我国古代,选美其实不是一件什么新鲜事,历朝历代都有从民间挑选美女的习惯,例如春秋时期的西施和汉代的王昭君都是从民间选上来的。但真正将其拔到关乎国之安危这个高度,还是明清两朝的事情古代被发配边疆的女囚犯能有多惨,不仅性命难保,还可能被羞辱?最近在互联网上掀起了一股,嘲讽自己的老家原来就是古代被发配的边疆的搞笑热潮,一众网友纷纷自嘲自己,看电视剧里那些罪犯被发配边疆时,还觉得大快人心,没成想这仔细一看,好家伙,原来发配天干物燥,小心火烛,你以为古代打更仅仅是为了报时?在各种古代题材的影视剧之中,更夫是一个常常会出现的角色,但不少人并不知道古代深夜为何要打更?难道安静地睡觉不好吗?事实上,古人的智慧是很多人所意想不到的。正所谓360行,行行出状元古代常见的通房丫鬟是怎样的存在?在封建社会时期,女子的地位是很低下的,常常受到很多规定及伦理制度的约束,特别是在明清时期。寻常富贵人家的女子都没有什么地位,那么那些穷苦人家的女子更是悲惨,一旦家里遇见意外困难,很