接觸abap新語法有一段時間了,看起來新語法吸收了很多java的特性,使代碼更加簡練,易讀(某些也不是那么易讀)。
眾所周知,abap是典型的面向對象語言,但是在我們的應用中,除了badi,oo=alv我們似乎很少使用到class等,但是就目前來看,oo肯定會成為abap的主流(如果abap能活下去)。
廢話不多說,RT所示,如何使用ABAP新語法+OO之300行代碼搞定AI五子棋,abap不是為了寫應用而生的語言,所以此篇只為練習所用,並非不務正業。
首先是設計,思路大概為:抽象出五子棋的方法,封裝成五子棋的class,利用salv模擬棋盤,內表模擬數組,圖標模擬棋子,選取估值算法為AI(這個好寫點,abap對算法支持沒那么好,我又比較菜)
其次是方法:落子(雙向)+判負+估值+棋盤繪制+初始化游戲
為大量節省代碼,還封裝了取棋盤任意位置的棋子類型的方法,大概看起來和數組一樣。
以下為class的設計。
"類定義 CLASS cl_gobang DEFINITION. PUBLIC SECTION. "SALV對象 DATA: mo_qp TYPE REF TO cl_salv_table. "常量:邊界,以及兩種棋子的圖標 CONSTANTS:border_ins TYPE c4 VALUE icon_border_inside."border CONSTANTS:player_wht TYPE c4 VALUE icon_incomplete. CONSTANTS:player_bak TYPE c4 VALUE icon_dummy. "步數,當前設定人先落子,所以步數為單數,輪到AI (可動態設定) DATA:step TYPE p VALUE 0. "核心表,存儲當前棋盤 "核心表設計思路: "需要模擬數組進行讀取操作,所以對於行列敏感,除去邊界,1-15行以及1-15列為棋盤,X為內表行,Y為列,列遞增尋址,所以要基於列名的可拼接 " 如C101,即為棋盤內表的第2列,棋盤的第一列,所以讀取方式為 it_qp[ X ]-|C {Y + 99}| => C {Y + 99}即為列名 " 以下當傳入參數為行列的i類型時,Y坐標為列的數字-99 DATA :BEGIN OF wa_qp, c100 TYPE c4, "border c101 TYPE c4,c102 TYPE c4,c103 TYPE c4,c104 TYPE c4,c105 TYPE c4, c106 TYPE c4,c107 TYPE c4,c108 TYPE c4,c109 TYPE c4,c110 TYPE c4, c111 TYPE c4,c112 TYPE c4,c113 TYPE c4,c114 TYPE c4,c115 TYPE c4, c116 TYPE c4, "border "該列為TYPE列,設定該列支持link事件,詳見初始化函數實現 ctyp TYPE salv_t_int4_column, END OF wa_qp. DATA:it_qp LIKE TABLE OF wa_qp. "初始化棋盤,17*17的內表 其中最外圍為邊界,所以棋盤大小為15*15 METHODS: ini_gobang. "棋盤繪制,調用SALV模擬棋盤 METHODS: dis_gobang. "落子算法,綁定SALV的link click事件 METHODS: set_gobang FOR EVENT link_click OF cl_salv_events_table IMPORTING row column. "AI核心算法,人落子之后,循環棋盤上所有未落子的點,調用估值算法估值,選擇估值最高的點落子(基於博弈樹的估值算法) METHODS: ai1_gobang." "模擬數組,並基於偏移方向以及偏移量的內表讀取,分8個方向 "偏移方向以及偏移量是可選的,當不傳入的時候,返回當前坐標的值 "返回值為基於偏移方向以及偏移量的內表字段值 METHODS: get_gobang IMPORTING row TYPE i col TYPE i off TYPE i OPTIONAL set TYPE i OPTIONAL "偏移量 RETURNING VALUE(value) TYPE c4. "AI核心估值算法,傳入該點坐標以及需要估值的玩家,返回該點的估值 METHODS: env_gobang IMPORTING row TYPE i col TYPE i player TYPE c4 RETURNING VALUE(value) TYPE i. "判負算法,通過對該點8個方向的延伸探索,判斷四條線的同類棋子數目 "返回值為以該點為原點,一條線上連續棋子的數量 METHODS: win_gobang IMPORTING row TYPE i off TYPE i column TYPE c4 RETURNING VALUE(score) TYPE i."判負算法 "消息 METHODS: mes_gobang IMPORTING mes TYPE string. ENDCLASS.
這部分是核心代碼,實現部分我將會之后附上。
首先棋子圖標的選取憑自己喜歡,以能分清為好,可以用icon事務碼或者showicon程序查看所有圖標代碼選擇自己喜歡的。
為了優化程序,我設計了17*17的數組(內表),最外一層為邊界,這樣可以不必擔心越界問題,讀到邊界圖標停止。
命名采用c1**的形式,所以讀取的時候格式統一,不會出問題。
一個很有意思的方法是基於偏移量的內表讀取,這其實是數組的概念,我們可以知道某位置的周邊棋子分布,用來估值計算AI以及判負使用。
至於每個方法的用處,代碼中注釋已經說明了一切,是的,即使要300行實現五子棋,我也沒有就省略注釋,注釋是一個很好的行為(雖然我也不喜歡寫)
以下為實現,有點長
"類的實現部分 CLASS cl_gobang IMPLEMENTATION. "判負算法 METHOD: win_gobang. score = 1. DATA(col_cur) = CONV i( column+1(3) - 99 ). ASSIGN COMPONENT column OF STRUCTURE it_qp[ row ] TO FIELD-SYMBOL(<pawn>) ."獲取當前棋子類型 "以下兩次循環,通過判斷方向以及偏移量判定同種棋子的類型, "當偏移量為正負時,8個方向可以看成四條線,所以調用時判斷四次即可, DO 4 TIMES. IF <pawn> NE get_gobang( row = row col = col_cur off = off set = sy-index ).EXIT.ENDIF."若該方向遇見不同棋子,直接結束循環,下同 score = score + 1. ENDDO. "一條線的另一方向 DO 4 TIMES. IF <pawn> NE get_gobang( row = row col = col_cur off = off set = - sy-index ).EXIT.ENDIF. score = score + 1. ENDDO. ENDMETHOD. * 基於偏移向量以及偏移量的讀取 METHOD: get_gobang. DATA(row_now) = row. DATA(col_now) = col. CASE off. WHEN 1.row_now = row_now + set. " 方向圖示 0為初始點 WHEN 2.col_now = col_now + set. " 7 2 8 WHEN 3.row_now = row_now - set. " WHEN 4.col_now = col_now - set. " WHEN 5.row_now = row_now + set.col_now = col_now + set. " 4 0 3 WHEN 6.row_now = row_now + set.col_now = col_now - set. " WHEN 7.row_now = row_now - set.col_now = col_now - set. " WHEN 8.row_now = row_now - set.col_now = col_now + set. " 6 1 5 WHEN OTHERS. ENDCASE. * IF col_now < 1 OR col_now > 16 OR row_now < 1 OR row_now > 17. * value = border_ins.RETURN. * ENDIF. "返回基於方向以及偏移量的值,如果assign失敗,那么為設定該值為邊界 ASSIGN COMPONENT col_now OF STRUCTURE it_qp[ row_now ] TO FIELD-SYMBOL(<pawn_cur>). value = COND #( WHEN <pawn_cur> IS ASSIGNED THEN <pawn_cur> ELSE border_ins ). ENDMETHOD. "初始化棋盤 為帶邊界的17*17內表,其中內部15*15位空。詳情可調試並觀察結果 METHOD: ini_gobang. "雙層循環得到2-16行 it_qp = VALUE #( FOR j = 1 UNTIL j > 15 ( c100 = border_ins c116 = border_ins "該循環得到Ctyp列,該列為表結構,存儲設置為link事件的所有列 ctyp = VALUE #( FOR i = 1 UNTIL i > 15 ( columnname = |C{ i + 100 }| value = if_salv_c_cell_type=>hotspot ) ) ) ). "循環得到一個邊界工作區 DO 17 TIMES. ASSIGN COMPONENT sy-index OF STRUCTURE wa_qp TO FIELD-SYMBOL(<value>). <value> = border_ins. ENDDO. "將上下邊界放入內表,初始化完畢 INSERT wa_qp INTO it_qp INDEX 1. APPEND wa_qp TO it_qp. ENDMETHOD. "彈出消息,通常為獲勝或者平局時,初始化棋盤重新開始 METHOD: mes_gobang. MESSAGE mes TYPE 'I'. ini_gobang( ). dis_gobang( ). ENDMETHOD. "主顯示方法 METHOD: dis_gobang. "如果SALV對象不存在(第一次繪制) IF mo_qp IS NOT BOUND. ini_gobang( )."初始化棋盤 "通過內表獲取SALV對象 cl_salv_table=>factory( IMPORTING r_salv_table = mo_qp CHANGING t_table = it_qp ). * mo_qp->get_functions( )->set_all( )."act all functions * mo_qp->get_functions( )->add_function( "附加按鈕只能用於可控模式,所以會dump * name = 'NEWGAME' "icon = l_icon * text = 'New Game' * tooltip = 'New Game' * position = if_salv_c_function_position=>right_of_salv_functions ). "分配set_bang方法綁定到alv對象的事件 事件獲取:mo_qp->get_event( ) SET HANDLER me->set_gobang FOR mo_qp->get_event( ). "獲取所有列對象 DATA(gr_columns) = mo_qp->get_columns( ). "設定CTYP列為格式列(該列存儲link事件,所以值被設定為Hotspot,詳情見初始化) gr_columns->set_cell_type_column( 'CTYP' ). "循環獲取所有的列,設置輸出長度為2,並且居中顯示,以便看上去像個棋盤 DO 17 TIMES. DATA(gr_column) = gr_columns->get_column( CONV lvc_fname( |C{ sy-index + 99 }| ) )."通過列合集獲取單列對象,輸入參數為列明名稱 gr_column->set_output_length( 2 )."輸出長度 gr_column->set_alignment( 3 )."居中顯示 ENDDO. "設定表頭以及其他信息,因刷新問題取消(需要重新設定) DATA(lo_header) = NEW cl_salv_form_layout_grid( ). DATA: lo_h_label TYPE REF TO cl_salv_form_label, lo_h_flow TYPE REF TO cl_salv_form_layout_flow. lo_h_label = lo_header->create_label( row = 1 column = 1 ). lo_h_label->set_text( 'GoBang!' ). "繪制表頭 mo_qp->set_top_of_list( lo_header ). "繪制ALV mo_qp->display( ). ELSE. "如果不是第一次顯示(刷新) 重新設定標題欄以及TYP列(不知為何refresh方法會把這些信息弄丟,官方文檔解釋refresh為rebuild) lo_header = NEW cl_salv_form_layout_grid( ). lo_h_label = lo_header->create_label( row = 1 column = 1 ). lo_h_label->set_text( 'GoBang!' ). mo_qp->get_columns( )->set_cell_type_column( 'CTYP' ). mo_qp->set_top_of_list( lo_header ). "基於行列的穩定刷新 mo_qp->refresh( s_stable = VALUE lvc_s_stbl( row = 'X' col = 'X') refresh_mode = 2 ). ENDIF. ENDMETHOD. "set方法, 落子 METHOD: set_gobang. "分配落子位置的值給指針,如果分配成功並且該處為空,即可落子 ASSIGN COMPONENT column OF STRUCTURE it_qp[ row ] TO FIELD-SYMBOL(<pawn>). CHECK <pawn> IS INITIAL AND <pawn> IS ASSIGNED. "通過setp的奇偶性判斷落子的值 <pawn> = COND #( WHEN step MOD 2 EQ 1 THEN player_wht ELSE player_bak ). "停頓一秒 避免不必要的麻煩 WAIT UP TO 1 SECONDS. step = step + 1."步數累加 dis_gobang( ). "刷新棋盤 "每走一步,傳入當前落子點進行勝負判斷 分別基於1236四條線,方向設定見get_gobang注釋。 IF win_gobang( row = row column = CONV #( column ) off = 1 ) >= 5 OR win_gobang( row = row column = CONV #( column ) off = 2 ) >= 5 OR win_gobang( row = row column = CONV #( column ) off = 5 ) >= 5 OR win_gobang( row = row column = CONV #( column ) off = 6 ) >= 5. "如果某條線的連續棋子總數大於5,彈出獲勝消息並初始化棋盤 mes_gobang( COND #( WHEN step MOD 2 EQ 1 THEN |You Win!| ELSE |You Lost!| ) ). EXIT. ENDIF. "如果步數step = 15*15,說明雙方落子已經滿了,並且沒有分勝負,平局提示並退出 IF step = 15 * 15. mes_gobang( |No Win!| ). EXIT. ENDIF. "如果setp為奇數,那么剛剛落子的是玩家,此時調用AI算法落子 CHECK step MOD 2 EQ 1. ai1_gobang( ). ENDMETHOD. "核心算法之AI "算法思路:循環棋盤上未落子的所有點進行估值,並且返回得分最高的點,調用set方法落子 "算法分進攻和防守,可以通過微調分數設定AI智力以及走棋方式 "可以加隨機數算法使AI看起來多樣化一些 METHOD: ai1_gobang. "best x y為最佳落子位置 DATA:best_x TYPE i, best_y TYPE i. "max為最高分數 因為不需要排序,只取最高,所以冒泡即可 DATA:max TYPE i VALUE 0. LOOP AT it_qp INTO wa_qp. DATA(x1) = sy-tabix."臨時變量存儲內表序列(調用其他方法會改變該值) "只循環中間的15*15即可 DO 16 TIMES. DATA(y1) = sy-index."臨時變量存儲do序列(不知道會不會變,保險起見用了臨時變量) IF get_gobang( row = x1 col = y1 ) IS NOT INITIAL."如果該點已被落子,跳出循環繼續 CONTINUE. ENDIF. "進攻:對白色棋子進行估值,如果分數大約此前的分數,那么替換掉該分數,並存儲當前點為best IF max < env_gobang( row = x1 col = y1 player = player_wht ). max = env_gobang( row = x1 col = y1 player = player_wht ). best_x = x1. best_y = y1. ENDIF. "防守:對黑色棋子進行估值,如果分數大約此前的分數,那么替換掉該分數,並存儲當前點為best IF max <= env_gobang( row = x1 col = y1 player = player_bak ). max = env_gobang( row = x1 col = y1 player = player_bak ). best_x = x1. best_y = y1. ENDIF. ENDDO. ENDLOOP. "獲取到best落子點后,調用set函數落子, set_gobang( row = CONV #( best_x ) column = CONV #( |C{ best_y + 99 }| ) ). ENDMETHOD. "AI核心之估值算法 "估值算法一般來說取決於條件的齊備,但是即使條件都沒問題,AI的智商看起來也不怎么高, "這是因為深度不夠,如果估值的深度延伸到周圍偏移3,就會比較厲害,但是執行慢,並且很麻煩寫, "以下對估值算法做了一些適度優化,雖然看起來沒什么用 "傳入需要估值點的坐標,需要估值的棋子類型 METHOD: env_gobang."X Y PLAYER "定義對手 DATA(opsite) = COND #( WHEN player = player_wht THEN player_bak ELSE player_wht ). "中心點的估值可以適當增加,這是對估值算法的優化1 value = 16 - abs( row - 8 ) - abs( col - 8 ). "對8個方向的棋子循環,通過某方向的棋子數量進行相關估值 "棋盤類型大致為活四,死四,活三,死三,活二,死二 DO 8 TIMES. DATA(i) = sy-index. * // *11110 ("活四" 必勝 設定為最高分數) IF ( get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) = player AND get_gobang( row = row col = col off = i set = 3 ) = player AND get_gobang( row = row col = col off = i set = 4 ) = player AND get_gobang( row = row col = col off = i set = 5 ) IS INITIAL ). value = value + 4500000. "優化2,測試BUG,當雙方都有活四時,AI進攻會贏,卻選擇防守,所以輸掉,所以輪到AI時要增加該點估值 IF player EQ player_wht.value = value + 100000.ENDIF. ENDIF. * 死四A 21111* IF ( get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) = player AND get_gobang( row = row col = col off = i set = 3 ) = player AND get_gobang( row = row col = col off = i set = 4 ) = player AND ( get_gobang( row = row col = col off = i set = 5 ) = opsite OR get_gobang( row = row col = col off = i set = 5 ) = border_ins ) ). value = value + 300000. ENDIF. * 死四B 111*1 IF ( get_gobang( row = row col = col off = i set = -1 ) = player AND get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) = player AND get_gobang( row = row col = col off = i set = 3 ) = player AND ( get_gobang( row = row col = col off = i set = 4 ) = opsite OR get_gobang( row = row col = col off = i set = 4 ) = border_ins ) ). value = value + 300000. ENDIF. * 死四C 11*11 IF ( get_gobang( row = row col = col off = i set = -1 ) = player AND get_gobang( row = row col = col off = i set = -2 ) = player AND get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) = player AND ( get_gobang( row = row col = col off = i set = 3 ) = opsite OR get_gobang( row = row col = col off = i set = 3 ) = border_ins ) ). value = value + 300000. ENDIF. * // 2111* (活三) IF ( get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) = player AND get_gobang( row = row col = col off = i set = 3 ) = player AND get_gobang( row = row col = col off = i set = 4 ) IS INITIAL ). value = value + 200000. ENDIF. * // 211* (活2) IF ( get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) = player AND get_gobang( row = row col = col off = i set = 3 ) IS INITIAL ). value = value + 100000. ENDIF. * // 死三 11*1 2111* IF ( get_gobang( row = row col = col off = i set = -1 ) = player AND get_gobang( row = row col = col off = i set = -2 ) = player AND get_gobang( row = row col = col off = i set = 1 ) = opsite ) OR ( get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) = player AND get_gobang( row = row col = col off = i set = -1 ) = opsite ). value = value + 80000. ENDIF. *// 判斷是否存在 1*001(死二) IF ( get_gobang( row = row col = col off = i set = 1 ) = player AND get_gobang( row = row col = col off = i set = 2 ) IS INITIAL AND get_gobang( row = row col = col off = i set = 3 ) IS INITIAL ). value = value + 100. ENDIF. * 優化3 附近點比較多的時候適當增加權重 IF ( get_gobang( row = row col = col off = i set = -1 ) IS NOT INITIAL AND get_gobang( row = row col = col off = i set = -1 ) <> border_ins ) OR ( get_gobang( row = row col = col off = i set = 1 ) IS NOT INITIAL AND get_gobang( row = row col = col off = i set = 1 ) <> border_ins ). value = value + 25. ENDIF. ENDDO. "增加隨機數,適當改變攻防分數,獲取不同玩法(微調) "可能會變得更智障 DATA ran TYPE i. CALL FUNCTION 'QF05_RANDOM_INTEGER' EXPORTING ran_int_max = 2 ran_int_min = 1 IMPORTING ran_int = ran. value = COND #( WHEN ran EQ 1 THEN value * 99 / 100 ELSE value * 101 / 100 ). ENDMETHOD. ENDCLASS.
大家都是成熟的abap了,實現部分的代碼貼上來大家都能看懂,我就不多解釋了(相信注釋已經解釋了一切)
嘻嘻我對估值算法做了一些優化哦~~,這是AI的核心,但是大家不必非要搞懂這個。
最后調用即可:
"主進程 實例化類並調用顯示即可 DATA(gobang) = NEW cl_gobang( ). gobang->dis_gobang( ).
so,代碼就寫完了,以上代碼直接全復制到se38里可以直接執行哦,我確定沒有遺漏什么東西,如果有,那就是TYPES c4(4) TYPE c.的定義了。
如果還有,別讓代碼覆蓋了你的report頭。
也許我該附上游戲截圖,如果有時間的話再改吧。