OpenAI Gym使用、rendering画图

gym开源库:包含一个测试问题集,每个问题成为环境(environment),可以用于自己的RL算法开发。这些环境有共享的接口,允许用户设计通用的算法。其包含了deep mind 使用的Atari游戏测试床。

在强化学习中有2个基本概念,一个是环境(environment),称为外部世界,另一个为智能体agent(写的算法)。agent发送action至environment,environment返回观察和回报。

Gym官方文档

Hello gym

import gym
# 创建一个小车倒立摆模型
env = gym.make(‘CartPole-v0’)
# 初始化环境
env.reset()
# 刷新当前环境,并显示
for _ in range(1000):
    env.render()
    env.step(env.action_space.sample()) # take a random action

[图片上传失败...(image-f6cf84-1570442880862)]

设计理念图,一个环境的step函数返回需要的信息,有4种返回值

  • observation
  • reward
  • done :判断是否到了重新设定(reset)环境
  • info :用于调试的诊断信息,有时也用于学习,但智能体(agent )在正式的评价中不允许使用该信息进行学习。

该进程通过调用reset()来启动,它返回一个初始observation。 所以之前代码的更恰当的方法是遵守done的标志:

空间(Spaces)

在上面的例子中,已经从环境的动作空间中抽取随机动作。但这些行动究竟是什么呢? 每个环境都带有action_spaceobservation_space对象。这些属性是Space类型,它们描述格式化的有效的行动和观察。

import gym
env = gym.make('CartPole-v0')
# 离散空间允许固定范围的非负数,因此在这种情况下,有效的动作是0或1. 
print(env.action_space)
#> Discrete(2)
print(env.observation_space)
#> Box(4,)

Box空间表示一个n维box,所以有效的观察将是4个数字的数组。 也可以检查Box的范围:

print(env.observation_space.high)
#> array([ 2.4       ,         inf,  0.20943951,         inf])
print(env.observation_space.low)
#> array([-2.4       ,        -inf, -0.20943951,        -inf])

这种内省可以帮助编写适用于许多不同环境的通用代码。box和discrete是最常见的空间。你可以从一个空间中取样,或者检查某物是否属于它:

from gym import spaces
space = spaces.Discrete(8) # Set with 8 elements {0, 1, 2, ..., 7}
x = space.sample()
assert space.contains(x)
assert space.n == 8

Env.render画图

参考Gym 简单画图

# 首先,导入库文件(包括gym模块和gym中的渲染模块)
import gym
from gym.envs.classic_control import rendering

# 我们生成一个类,该类继承 gym.Env. 同时,可以添加元数据,改变渲染环境时的参数
class Test(gym.Env):
    # 如果你不想改参数,下面可以不用写
    metadata = {
        'render.modes': ['human', 'rgb_array'],
        'video.frames_per_second': 2
    }
# 我们在初始函数中定义一个 viewer ,即画板
    def __init__(self):
        self.viewer = rendering.Viewer(600, 400)   # 600x400 是画板的长和框
    # 继承Env render函数
    def render(self, mode='human', close=False):
        # 下面就可以定义你要绘画的元素了
        line1 = rendering.Line((100, 300), (500, 300))
        line2 = rendering.Line((100, 200), (500, 200))
        # 给元素添加颜色
        line1.set_color(0, 0, 0)
        line2.set_color(0, 0, 0)
        # 把图形元素添加到画板中
        self.viewer.add_geom(line1)
        self.viewer.add_geom(line2)

        return self.viewer.render(return_rgb_array=mode == 'rgb_array')
    
# 最后运行
if __name__ == '__main__':
    t = Test()
    while True:
        t.render()

△.值得注意的是,画板的水平方向是 x 轴, 垂直方向是 y 轴, 且原点在左下角

画个圆

    def render(self, mode='human', close=False):
        # 画一个直径为 30 的园
        circle = rendering.make_circle(30)
        
        # 添加一个平移操作
        circle_transform = rendering.Transform(translation=(100, 200))
        # 让圆添加平移这个属性,
        circle.add_attr(circle_transform)
        
        self.viewer.add_geom(circle)
        return self.viewer.render(return_rgb_array=mode == 'rgb_array')

△注意.是圆心在平移

RingViewr

研究rings时写的render

import gym
from gym.envs.classic_control import rendering
import time
import numpy as np
import random


class ringViewer(rendering.Viewer):
    '''
    画板,直接继承自rendering.Viewer
    '''
    def __init__(self,width, height, display=None):
        super(ringViewer, self).__init__(width, height, display=None)

    @staticmethod
    def pos2loc(pos=0):
        '''
        根据位置索引确定画图坐标
        :param pos: 位置索引0-9
        :return: loc
        '''
        pass

    @staticmethod
    def getSize(size):
        '''
        设置画圆的半径
        :param size:[0-2]
        :return: radius
        '''
        pass

    @staticmethod
    def getColor(c=0):
        '''
        根据颜色索引选择圆圈颜色
        :param c:
        :return: list
        '''
        pass
    
    def drawNewring(self, newring:list=None):
        '''
        画新生成的圆
        :param newring:
        :return:
        '''
        for i in range(len(newring)):
            if newring[i] != 0:
                ring = rendering.make_circle(radius=self.getSize(i),
                                             res=50,
                                             filled=False)
                r, g, b = self.getColor(newring[i])
                ring.set_color(r, g, b)
                ring_transform = rendering.Transform(translation=(150,30))
                ring.add_attr(ring_transform)
                self.add_geom(ring)

    def _drawQG(self, qgs: list=None):
        '''
        画棋盘上各个棋格的圆圈
        :param qgs:
        :return: None
        '''
        for num,qg in enumerate(qgs):
            for i in range(len(qg)):
                if qg[i] != 0:
                    ring = rendering.make_circle(radius=self.getSize(i),
                                                 res = 50,
                                                 filled=False)
                    r, g, b = self.getColor(qg[i])
                    ring.set_color(r, g, b)
                    ring_transform = rendering.Transform(translation=self.pos2loc(num))
                    ring.add_attr(ring_transform)
                    self.add_geom(ring)

    def getQG(self, qg: list=None):
        '''
        将len=27的list转换为[[],[],...]
        :param qg: (27,1)的list
        :return: (9,1)的list
        '''
        qgs = []
        for x in range(3):
            for y in range(3):
                tmp = []
                for z in range(3):
                    tmp.append(qg[9*x+3*y+z])
                qgs.append(tmp)
        self._drawQG(qgs)


class Testenv(gym.Env):
    # 如果你不想改参数,下面可以不用写
    metadata = {
        'render.modes': ['human', 'rgb_array'],
        'video.frames_per_second': 2
    }

    def __init__(self):
        self.viewer = ringViewer(300, 400)   # 600x400 是画板的长和框
        self.state:list = []
        self.state:list = []

    def setState(self, state):
        self.state = state

    def setNewring(self, newring=None):
        self.newring = newring

    def render(self, mode='human', close=False):
        # 由于没有找到viewer源码中删除组件的代码,于是每次在渲染前 清空上一次geoms和onetime_geoms列表 来达到消除的目的
        if self.state.any():
            self.viewer.geoms.clear()
            self.viewer.onetime_geoms.clear()
            self.viewer.getQG(self.state)
        if self.newring:
            self.viewer.drawNewring(self.newring)

        return self.viewer.render(return_rgb_array=mode == 'rgb_array')


if __name__ == '__main__':
    v = Testenv()
    while True:
        v.setState(np.random.randint(0,6,(27)))
        v.setNewring([random.randint(0,5) for x in range(3)])
        print(v.state)
        print(v.newring)
        v.render()
        time.sleep(2)

△.由于没有找到viewer源码中删除组件的代码,于是每次在渲染前 清空上一次geoms和onetime_geoms列表 来达到消除的目的

效果图如下

ring.jpg

深入剖析gym环境构建[转]

由于该博客的代码展示实在太乱,于是重新帮他排版了一下

我们继续讲,从第1小节的尾巴开始。有三个重要的函数:

  • env = gym.make(‘CartPole-v0’)
  • env.reset()
  • env.render()

第一个函数是创建环境,我们会在第3小节具体讲如何创建自己的环境,所以这个函数暂时不讲。第二个函数env.reset()和第三个函数env.render()是每个环境文件都包含的函数。我们以cartpole为例,对这两个函数进行讲解。

Cartpole的环境文件在~你的gym目录/gym/envs/classic_control/cartpole.py.

该文件定义了一个CartPoleEnv的环境类,该类的成员函数有:seed(), step(),reset()和render(). 第1小节调用的就是CartPoleEnv的两个成员函数reset()和render()。下面,我们先讲讲这两个函数,再介绍step()函数

2.1 reset()函数详解

reset()为重新初始化函数。那么这个函数有什么用呢?

在强化学习算法中,智能体需要一次次地尝试,累积经验,然后从经验中学到好的动作。一次尝试我们称之为一条轨迹或一个episode. 每次尝试都要到达终止状态. 一次尝试结束后,智能体需要从头开始,这就需要智能体具有重新初始化的功能。函数reset()就是这个作用。

reset()的源代码为:

def _reset()
    # 利用均匀随机分布初试化环境的状态
    self.state = self.np_random.uniform(low=-0.05, high=0.05, size=(4,))
    # 设置当前步数为None
    self.steps_beyond_done = None
    # 返回环境的初始化状态
    return np.array(self.state)

2.2 render()函数详解

render()函数在这里扮演图像引擎的角色。一个仿真环境必不可少的两部分是物理引擎图像引擎。物理引擎模拟环境中物体的运动规律;图像引擎用来显示环境中的物体图像。其实,对于强化学习算法,该函数可以没有。但是,为了便于直观显示当前环境中物体的状态,图像引擎还是有必要的。另外,加入图像引擎可以方便我们调试代码。下面具体介绍gym如何利用图像引擎来创建图像。

我们直接看源代码:

from gym.envs.classic_control import rendering
# 这一句导入rendering模块,利用rendering模块中的画图函数进行图形的绘制
class myenv(gym.Env)
    def _render(self, mode=’human’, close=False):
        if close:
            pass #省略,直接看关键代码部分
        if self.viewer is None:
            # 如绘制600*400的窗口函数为:
            self.viewer = rendering.Viewer(screen_width, screen_height)
        # 其中screen_width=600, screen_height=400
        # 创建小车的代码为:
            l,r,t,b = -cartwidth/2, cartwidth/2, cartheight/2, -cartheight/2
            axleoffset =cartheight/4.0
            cart = rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
        # 其中rendering.FilledPolygon为填充一个矩形。

创建完cart的形状,接下来给cart添加平移属性和旋转属性。将车的位移设置到cart的平移属性中,cart就会根据系统的状态变化左右运动。具体代码解释,我已上传到github上面了,gxnk/reinforcement-learning-code 。想深入了解的同学可去下载学习。

2.3 step()函数详解

该函数在仿真器中扮演物理引擎的角色。其输入是动作a,输出是:下一步状态,立即回报,是否终止,调试项。

该函数描述了智能体与环境交互的所有信息,是环境文件中最重要的函数。在该函数中,一般利用智能体的运动学模型和动力学模型计算下一步的状态和立即回报,并判断是否达到终止状态。

我们直接看源代码:

def _step(self, action):
    assert self.action_space.contains(action), "%r (%s) invalid"%(action, type(action))
    state = self.state
    x, x_dot, theta, theta_dot = state #系统的当前状态
    force = self.force_mag if action==1 else -self.force_mag #输入动作,即作用到车上的力
    costheta = math.cos(theta) #余弦函数
    sintheta = math.sin(theta) #正弦函数
    #底下是车摆的动力学方程式,即加速度与动作之间的关系。
    temp = (force + self.polemass_length * theta_dot * theta_dot * sintheta) / self.total_mass
    thetaacc = (self.gravity * sintheta - costheta* temp) / (self.length * (4.0/3.0 - self.masspole * costheta * costheta / self.total_mass)) #摆的角加速度
    xacc = temp - self.polemass_length * thetaacc * costheta / self.total_mass #小车的平移加速
    x = x + self.tau * x_dot
    x_dot = x_dot + self.tau * xacc
    theta = theta + self.tau * theta_dot
    theta_dot = theta_dot + self.tau * thetaacc #积分求下一步的状态
    self.state = (x,x_dot,theta,theta_dot)

2.4 一个简单的demo

下面,我给出一个最简单的demo,让大家体会一下上面三个函数如何使用。

import gym
import time
env = gym.make('CartPole-v0')   
#创造环境observation = env.reset()       
#初始化环境,observation为环境状态
count = 0
for t in range(100):    
    action = env.action_space.sample()  
    #随机采样动作    
    observation, reward, done, info = env.step(action)  
    #与环境交互,获得下一步的时刻    
    if done:                     
        break    
    env.render()         
    #绘制场景    
    count+=1    
    time.sleep(0.2)      
    #每次等待0.2s
    print(count)             
    #打印该次尝试的步数

第3小节:创建自己的gym环境并利示例qlearning的方法

在上一小节中以cartpole为例子深入剖析了gym环境文件的重要组成。我们知道,一个gym环境最少的组成需要包括reset()函数和step()函数。当然,图像显示函数render()一般也是需要的。这一节,我会以机器人找金币为例给大家演示如何构建一个全新的gym环境,并以此环境为例,示例最经典的强化学习算法qlearning算法。在3.1节中,给出机器人找金币的问题陈述;第3.2节中,给出构建gym环境的过程;第3.3节中,利用qlearning方法实现机器人找金币的智能决策。全部代码已传到github上。

3.1 机器人找金币的问题陈述

img

图1.1 机器人找金币

如图1.1 为机器人在网格世界找金币的示意图。该网格世界一共有8个状态,其中状态6和状态8为死亡区域,状态7为金币区域。机器人的初始位置为网格世界中任意一个状态。机器人从初始状态出发寻找金币。机器人进行一次探索,进入死亡区域或找到金币,本次探测结束。机器人找到金币的回报为1,进入死亡区域回报为-1,机器人在区域1-5之间转换时,回报为0。我们的目标是找到一个策略使得机器人不管处在什么状态(1-5)都能找到金币。对于这个机器人找金币的游戏,我们可以利用强化学习的方法来实现。

构建网格世界的gym环境

该例子的代码,除了本篇博客有以外,OpenAI Gym构建自定义强化学习环境有更仔细和规范的代码贴出

一个gym的环境文件,其主体是个类,在这里我们定义类名为:GridEnv, 其初始化为环境的基本参数,因为机器人找金币的过程是一个马尔科夫过程,我们在强化学习入门课程的第一讲已经介绍过了一个马尔科夫过程应该包括状态空间,动作空间,回报函数,状态转移概率。因此,我们在类GridEnv的初始化时便给出了相应的定义。网格世界的全部代码在gxnk/reinforcement-learning-code,文件名为 grid_mdp.py. 我们看源代码:

# 状态空间为:
self.states = [1,2,3,4,5,6,7,8]
# 动作空间为:
  self.actions = ['n','e','s','w']
# 回报函数为:
  self.rewards = dict(); #回报的数据结构为字典
  self.rewards['1_s'] = -1.0
  self.rewards['3_s'] = 1.0
    self.rewards['5_s'] = -1.0
# 状态转移概率为:
  self.t = dict(); #状态转移的数据格式为字典
  self.t['1_s'] = 6
  self.t['1_e'] = 2
  self.t['2_w'] = 1
  self.t['2_e'] = 3
  self.t['3_s'] = 7
  self.t['3_w'] = 2
  self.t['3_e'] = 4
  self.t['4_w'] = 3
  self.t['4_e'] = 5
  self.t['5_s'] = 8
  self.t['5_w'] = 4

有了状态空间,动作空间和状态转移概率,我们便可以写step(a)函数了。这里特别注意的是,step()函数的输入是动作,输出为:下一个时刻的动作,回报,是否终止,调试信息。尤其需要注意的是输出的顺序不要弄错了。对于调试信息,可以为空,但不能缺少,否则会报错,常用{}来代替。我们看源代码:

step函数的建立:

def _step(self, action):
    #系统当前状态
    state = self.state
      #判断系统当前状态是否为终止状态
    if state in self.terminate_states:
        return state, 0, True, {}
    key = "%d_%s"%(state, action) #将状态和动作组成字典的键值
    #状态转移
    if key in self.t:
        next_state = self.t[key]
    else:
        next_state = state
        self.state = next_state
        is_terminal = False
    if next_state in self.terminate_states:
        is_terminal = True
    if key not in self.rewards:
        r = 0.0
    else:
        r = self.rewards[key]
    return next_state, r,is_terminal,{}

step()函数就是这么简单。下面我们重点介绍下如何写render()函数。从图1.1机器人找金币的示意图我们可以看到,网格世界是由一些线和圆组成的。因此,我们可以调用rendering中的画图函数来绘制这些图像。

render函数的建立:

整个图像是一个600*400的窗口,可用如下代码实现:

from gym.envs.classic_control import rendering

self.viewer = rendering.Viewer(screen_width, screen_height)
# 创建网格世界,一共包括11条直线,事先算好每条直线的起点和终点坐标,然后绘制这些直线,代码如下:
#创建网格世界
def render(self):
    self.line1 = rendering.Line((100,300),(500,300))
    self.line2 = rendering.Line((100, 200), (500, 200))
    self.line3 = rendering.Line((100, 300), (100, 100))
    self.line4 = rendering.Line((180, 300), (180, 100))
    self.line5 = rendering.Line((260, 300), (260, 100))
    self.line6 = rendering.Line((340, 300), (340, 100))
    self.line7 = rendering.Line((420, 300), (420, 100))
    self.line8 = rendering.Line((500, 300), (500, 100))
    self.line9 = rendering.Line((100, 100), (180, 100))
    self.line10 = rendering.Line((260, 100), (340, 100))
    self.line11 = rendering.Line((420, 100), (500, 100))
    # 接下来,创建死亡区域,我们用黑色的圆圈代表死亡区域,源代码如下:

    # 创建第一个骷髅
    self.kulo1 = rendering.make_circle(40)
    self.circletrans = rendering.Transform(translation=(140,150))
    self.kulo1.add_attr(self.circletrans)
    self.kulo1.set_color(0,0,0)
    # 创建第二个骷髅
    self.kulo2 = rendering.make_circle(40)
    self.circletrans = rendering.Transform(translation=(460, 150))
    self.kulo2.add_attr(self.circletrans)
    self.kulo2.set_color(0, 0, 0)
    # 创建金币区域,用金色的圆来表示:
    # 创建金条
    self.gold = rendering.make_circle(40)
    self.circletrans = rendering.Transform(translation=(300, 150))
    self.gold.add_attr(self.circletrans)
    self.gold.set_color(1, 0.9, 0)

    # 创建机器人,我们依然用圆来表示机器人,为了跟死亡区域和金币区域不同,我们可以设置不同的颜色:
    # 创建机器人
    self.robot= rendering.make_circle(30)
    self.robotrans = rendering.Transform()
    self.robot.add_attr(self.robotrans)
    self.robot.set_color(0.8, 0.6, 0.4)
    # 创建完之后,给11条直线设置颜色,并将这些创建的对象添加到几何中代码如下:
    self.line1.set_color(0, 0, 0)
    self.line2.set_color(0, 0, 0)
    self.line3.set_color(0, 0, 0)
    self.line4.set_color(0, 0, 0)
    self.line5.set_color(0, 0, 0)
    self.line6.set_color(0, 0, 0)
    self.line7.set_color(0, 0, 0)
    self.line8.set_color(0, 0, 0)
    self.line9.set_color(0, 0, 0)
    self.line10.set_color(0, 0, 0)
    self.line11.set_color(0, 0, 0)
    # 添加组件到Viewer中
    self.viewer.add_geom(self.line1)
    self.viewer.add_geom(self.line2)
    self.viewer.add_geom(self.line3)
    self.viewer.add_geom(self.line4)
    self.viewer.add_geom(self.line5)
    self.viewer.add_geom(self.line6)
    self.viewer.add_geom(self.line7)
    self.viewer.add_geom(self.line8)
    self.viewer.add_geom(self.line9)
    self.viewer.add_geom(self.line10)
    self.viewer.add_geom(self.line11)
    self.viewer.add_geom(self.kulo1)
    self.viewer.add_geom(self.kulo2)
    self.viewer.add_geom(self.gold)
    self.viewer.add_geom(self.robot)
    # 接下来,开始设置机器人的位置。机器人的位置根据其当前所处的状态不同,所在的位置不同。我们事先计算出每个状态处机器人位置的中心坐标,并存储到两个向量中,并在类初始化中给出:
        self.x=[140,220,300,380,460,140,300,460]
        self.y=[250,250,250,250,250,150,150,150]
    # 根据这两个向量和机器人当前的状态,我们就可以设置机器人当前的圆心坐标了即:

        if self.state is None: return None

        self.robotrans.set_translation(self.x[self.state-1], self.y[self.state- 1])

    # 最后还需要一个返回语句:
        return self.viewer.render(return_rgb_array=mode == 'rgb_array')

以上便完成了render()函数的建立

reset()函数的建立:

reset()函数常常用随机的方法初始化机器人的状态,即:

def _reset(self):
    self.state = self.states[int(random.random() * len(self.states))]
    return self.state

环境的注册

全部的代码请去github上下载学习。下面重点讲一讲如何将建好的环境进行注册,以便通过gym的标准形式进行调用。其实环境的注册很简单,只需要3步:

第一步:将我们自己的环境文件(我创建的文件名为grid_mdp.py)拷贝到你的gym安装目录/gym/gym/envs/classic_control文件夹中。(拷贝在这个文件夹中因为要使用rendering模块。当然,也有其他办法。该方法不唯一)

第二步:打开该文件夹(第一步中的文件夹)下的init.py文件,在文件末尾加入语句:from gym.envs.classic_control.grid_mdp import GridEnv

第三步:进入文件夹你的gym安装目录/gym/gym/envs,打开该文件夹下的init.py文件,添加代码:

register(
    # gym.make(‘id’)时的id
    id='GridWorld-v0',
    # 函数路口
    entry_point='gym.envs.classic_control:GridEnv',
    max_episode_steps=200,
    reward_threshold=100.0,
)

第一个参数id就是你调用gym.make(‘id’)时的id, 这个id你可以随便选取,我取的,名字是GridWorld-v0

第二个参数就是函数路口了。

后面的参数原则上来说可以不必要写。

经过以上三步,就完成了注册。

下面,我们给个简单的demo来测试下我们的环境的效果吧:

我们依然写个终端程序:

import gym

env = gym.make('GridWorld-v0')
env.reset()
env.render()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容